diff --git a/README.md b/README.md index 03a18e96..2769ce98 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Dash Logo](https://cloud.githubusercontent.com/assets/512416/2726786/6618d624-c5c2-11e3-9049-23637e5a1739.png)](https://github.com/Circular-Studios/Dash/wiki) -# [![Build Status](http://img.shields.io/travis/Circular-Studios/Dash/develop.svg?style=flat)](https://travis-ci.org/Circular-Studios/Dash) [![Docs](http://img.shields.io/badge/docs-ddoc-yellow.svg?style=flat)](http://circular-studios.github.io/Dash/docs/) [![Gitter Chat](http://img.shields.io/badge/chat-gitter-brightgreen.svg?style=flat)](https://gitter.im/Circular-Studios/Dash) [![Release](http://img.shields.io/github/release/Circular-Studios/Dash.svg?style=flat)](http://code.dlang.org/packages/dash) +# [![Build Status](http://img.shields.io/travis/Circular-Studios/Dash/develop.svg?style=flat)](https://travis-ci.org/Circular-Studios/Dash) [![Docs](http://img.shields.io/badge/docs-ddoc-yellow.svg?style=flat)](http://circular-studios.github.io/Dash/docs/v0.6.6) [![Gitter Chat](http://img.shields.io/badge/chat-gitter-brightgreen.svg?style=flat)](https://gitter.im/Circular-Studios/Dash) [![Release](http://img.shields.io/github/release/Circular-Studios/Dash.svg?style=flat)](http://code.dlang.org/packages/dash) If you're reading this page, chances are you fall into one of the following categories: diff --git a/dub.json b/dub.json index b39865b1..12017751 100644 --- a/dub.json +++ b/dub.json @@ -17,7 +17,8 @@ "derelict-assimp3": "~master", "dyaml": "~master", "gl3n-shared" : "~master", - "dlogg": ">=0.1.3" + "dlogg": "~>0.2.2", + "x11": { "version": "==1.0.0", "optional": true } }, "targetName": "dash", "targetType": "library", @@ -34,12 +35,5 @@ "libs-windows": [ "gdi32", "ole32", "kernel32", "user32", "comctl32", "comdlg32" - ], - - "dependencies-linux": { - "x11": { - "version": "~master", - "optional": true - } - } + ] } diff --git a/source/components/animation.d b/source/components/animation.d index 638f9337..eead71fd 100644 --- a/source/components/animation.d +++ b/source/components/animation.d @@ -1,38 +1,45 @@ /** - * TODO + * All classes dealing with using 3D skeletal animation */ module components.animation; import core.properties; import components.icomponent; -import utility.output; +import utility; import derelict.assimp3.assimp; import gl3n.linalg; /** - * TODO + * Animation object which handles all animation specific to the gameobject */ shared class Animation : IComponent { private: - shared AssetAnimation _animationData; - shared int _currentAnim; - shared float _currentAnimTime; - shared mat4[] _currBoneTransforms; - shared bool _animating; + /// Asset animation that the gameobject is animating based off of + AssetAnimation _animationData; + /// Current animation out of all the animations in the asset animation + int _currentAnim; + /// Current time of the animation + float _currentAnimTime; + /// Bone transforms for the current pose + mat4[] _currBoneTransforms; + /// If the gameobject should be animating + bool _animating; public: - /// TODO + /// Asset animation that the gameobject is animating based off of mixin( Property!_animationData ); - /// TODO + /// Current animation out of all the animations in the asset animation mixin( Property!_currentAnim ); - /// TODO + /// Current time of the animation mixin( Property!_currentAnimTime ); - /// TODO + /// Bone transforms for the current pose (Passed to the shader) mixin( Property!_currBoneTransforms ); + /// If the gameobject should be animating + mixin( Property!_animating ); /** - * TODO + * Create animation object based on asset animation */ this( shared AssetAnimation assetAnimation ) { @@ -43,43 +50,41 @@ public: } /** - * Updates the animation's bones. + * Updates the animation, updating time and getting a pose based on time */ override void update() { if( _animating ) { - // Update currentanimtime based on changeintime - _currentAnimTime += 0.002; + // Update currentanimtime based on deltatime and animations fps + _currentAnimTime += Time.deltaTime * 24.0f; - if( _currentAnimTime > 96.0f ) + if( _currentAnimTime >= 96.0f ) { _currentAnimTime = 0.0f; } - - // Calculate and store array of bonetransforms to pass to the shader - currBoneTransforms = animationData.getTransformsAtTime( _currentAnimTime ); } + + // Calculate and store array of bonetransforms to pass to the shader + currBoneTransforms = _animationData.getTransformsAtTime( _currentAnimTime ); } /** - * TODO - */ - override void shutdown() - { - + * Continue animating. + */ + void play() + { + _animating = true; } - /** - * Stops the animation from updating. + * Pause the animation */ void pause() { _animating = false; } - /** - * Stops the animation from updating and resets it. + * Stops the animation, moving to the beginning */ void stop() { @@ -88,291 +93,262 @@ public: } /** - * Allows animation to update. - */ - void play() - { - _animating = true; + * Shutdown the gameobjects animation data + */ + override void shutdown() + { + } } /** - * TODO + * Stores the animation skeleton/bones, stores the animations poses, and makes this information accessible to gameobjects */ shared class AssetAnimation { private: + /// List of animations, containing all of the information specific to each shared AnimationSet _animationSet; + /// Amount of bones shared int _numberOfBones; + bool _isUsed; public: - /// TODO + /// List of animations, containing all of the information specific to each mixin( Property!_animationSet ); - /// TODO + /// Amount of bones mixin( Property!_numberOfBones ); + /// Whether or not the material is actually used. + mixin( Property!( _isUsed, AccessModifier.Package ) ); /** - * TODO + * Create the assetanimation, parsing all of the animation data * * Params: - * - * Returns: + * animation = Assimp animation/poses object + * mesh = Assimp mesh/bone object + * boneHierarchy = Hierarchy of bones/filler nodes used for the animation */ this( const(aiAnimation*) animation, const(aiMesh*) mesh, const(aiNode*) boneHierarchy ) { _animationSet.duration = cast(float)animation.mDuration; _animationSet.fps = cast(float)animation.mTicksPerSecond; - - _animationSet.animNodes = makeNodesFromNode( animation, mesh, boneHierarchy.mChildren[ 1 ], null ); + + for( int i = 0; i < boneHierarchy.mNumChildren; i++) + { + string name = boneHierarchy.mChildren[ i ].mName.data.ptr.fromStringz; + + if( findBoneWithName( name, mesh ) != -1 ) + { + _animationSet.animBones = makeBonesFromHierarchy( animation, mesh, boneHierarchy.mChildren[ i ] ); + } + } } /** - * Each bone has one of two setups: - * Split up into five seperate nodes (translation -> preRotation -> Rotation -> Scale -> Bone) - * Or the bone is one node in the hierarchy + * Recurse the node hierarchy, parsing it into a usable bone hierarchy * - * Params: TODO + * Params: + * animation = Assimp animation/poses object + * mesh = Assimp mesh/bone object + * currNode = The current node checking in the hierarchy * - * Returns: TODO + * Returns: The bone based off of the currNode data */ - shared(Node) makeNodesFromNode( const(aiAnimation*) animation, const(aiMesh*) mesh, const(aiNode*) currNode, shared Node returnNode ) + shared(Bone) makeBonesFromHierarchy( const(aiAnimation*) animation, const(aiMesh*) mesh, const(aiNode*) currNode ) { - string name = cast(string)currNode.mName.data[ 0 .. currNode.mName.length ]; - int id = findNodeWithName( name, mesh ); - shared Node node; + //NOTE: Currently only works if each node is a Bone, works with bones without animation b/c of storing nodeOffset + //NOTE: Needs to be reworked to support this in the future + string name = currNode.mName.data.ptr.fromStringz; - if( id != -1 ) + int boneNumber = findBoneWithName( name, mesh ); + shared Bone bone; + + if( boneNumber != -1 && name ) { - logWarning( "Animation Node "); - node = new shared Node( name ); - node.id = id; - node.transform = convertAIMatrix( mesh.mBones[ node.id ].mOffsetMatrix ); + bone = new shared Bone( name, boneNumber ); - assignAnimationData( animation, node ); + bone.offset = convertAIMatrix( mesh.mBones[ bone.boneNumber ].mOffsetMatrix ); + bone.nodeOffset = convertAIMatrix( currNode.mTransformation ); + + assignAnimationData( animation, bone ); - returnNode = node; _numberOfBones++; } - else - { - node = returnNode; - } - // For each child node for( int i = 0; i < currNode.mNumChildren; i++ ) { - // Create it and assign to this node as a child - if( id != -1 ) - node.children ~= makeNodesFromNode( animation, mesh, currNode.mChildren[ i ], node ); + if( boneNumber != -1 ) + bone.children ~= makeBonesFromHierarchy( animation, mesh, currNode.mChildren[ i ] ); else - return makeNodesFromNode( animation, mesh, currNode.mChildren[ i ], node ); + return makeBonesFromHierarchy( animation, mesh, currNode.mChildren[ i ] ); } - return node; + return bone; } + /** + * Get a bone number by matching name bones in mesh + * + * Params: + * name = Name searching for + * mesh = Mesh containing bones to check + * + * Returns: Bone number of desired bone + */ + int findBoneWithName( string name, const(aiMesh*) mesh ) + { + for( int i = 0; i < mesh.mNumBones; i++ ) + { + if( name == mesh.mBones[ i ].mName.data.ptr.fromStringz ) + { + return i; + } + } + return -1; + } /** - * TODO + * Access the animation channels to find a match with the bone, then store its animation data * * Params: - * - * Returns: + * animation = Assimp animation/poses object + * boneToAssign = Bone to assign the animation keys/poses */ - void assignAnimationData( const(aiAnimation*) animation, shared Node nodeToAssign ) + void assignAnimationData( const(aiAnimation*) animation, shared Bone boneToAssign ) { - // For each bone animation data for( int i = 0; i < animation.mNumChannels; i++) { - const(aiNodeAnim*) temp = animation.mChannels[ i ]; - string name = cast(string)animation.mChannels[ i ].mNodeName.data[ 0 .. animation.mChannels[ i ].mNodeName.length ]; - // If the names match - if( checkEnd(name, "_$AssimpFbx$_Translation" ) && name[ 0 .. (animation.mChannels[ i ].mNodeName.length - 24) ] == nodeToAssign.name ) - { - nodeToAssign.positionKeys = convertVectorArray( animation.mChannels[ i ].mPositionKeys, - animation.mChannels[ i ].mNumPositionKeys ); - } - else if( checkEnd(name, "_$AssimpFbx$_Rotation" ) && name[ 0 .. animation.mChannels[ i ].mNodeName.length - 21 ] == nodeToAssign.name ) - { - nodeToAssign.rotationKeys = convertQuat( animation.mChannels[ i ].mRotationKeys, - animation.mChannels[ i ].mNumRotationKeys ); - } - else if( checkEnd(name, "_$AssimpFbx$_Scaling" ) && name[ 0 .. animation.mChannels[ i ].mNodeName.length - 20 ] == nodeToAssign.name ) - { - nodeToAssign.scaleKeys = convertVectorArray( animation.mChannels[ i ].mScalingKeys, - animation.mChannels[ i ].mNumScalingKeys ); - } - else if( name == nodeToAssign.name ) + string name = animation.mChannels[ i ].mNodeName.data.ptr.fromStringz; + + if( name == boneToAssign.name ) { // Assign the bone animation data to the bone - nodeToAssign.positionKeys = convertVectorArray( animation.mChannels[ i ].mPositionKeys, + boneToAssign.positionKeys = convertVectorArray( animation.mChannels[ i ].mPositionKeys, animation.mChannels[ i ].mNumPositionKeys ); - nodeToAssign.scaleKeys = convertVectorArray( animation.mChannels[ i ].mScalingKeys, + boneToAssign.scaleKeys = convertVectorArray( animation.mChannels[ i ].mScalingKeys, animation.mChannels[ i ].mNumScalingKeys ); - nodeToAssign.rotationKeys = convertQuat( animation.mChannels[ i ].mRotationKeys, - animation.mChannels[ i ].mNumRotationKeys ); + boneToAssign.rotationKeys = convertQuat( animation.mChannels[ i ].mRotationKeys, + animation.mChannels[ i ].mNumRotationKeys ); } } } /** - * Converts a aiVectorKey[] to vec3[]. + * Called by gameobject animation components to get an animation pose * * Params: + * time = The current animations time * - * Returns: - */ - shared( vec3[] ) convertVectorArray( const(aiVectorKey*) vectors, int numKeys ) - { - shared vec3[] keys; - for( int i = 0; i < numKeys; i++ ) - { - aiVector3D vector = vectors[ i ].mValue; - keys ~= vec3( vector.x, vector.y, vector.z ); - } - - return keys; - } - - /** - * Converts a aiQuatKey[] to quat[]. - * - * Params: - * - * Returns: + * Returns: The boneTransforms, returned to the gameobject animation component */ - shared( quat[] ) convertQuat( const(aiQuatKey*) quaternions, int numKeys ) + shared( mat4[] ) getTransformsAtTime( shared float time ) { - shared quat[] keys; - for( int i = 0; i < numKeys; i++ ) + shared mat4[] boneTransforms = new shared mat4[ _numberOfBones ]; + + // Check shader/model + for( int i = 0; i < _numberOfBones; i++) { - aiQuatKey quaternion = quaternions[ i ]; - keys ~= quat( quaternion.mValue.w, quaternion.mValue.x, quaternion.mValue.y, quaternion.mValue.z ); - int ii = 0; + boneTransforms[ i ] = mat4.identity; } + + fillTransforms( boneTransforms, _animationSet.animBones, time, mat4.identity ); - return keys; + return boneTransforms; } - /** - * Find bone with name in our structure. + * Recurse the bone hierarchy, filling up the bone transforms along the way * * Params: - * - * Returns: + * transforms = The boneTransforms to fill up + * bone = The current bone checking + * time = The animations current time + * parentTransform = The parents transform (which effects this bone) */ - int findNodeWithName( string name, const(aiMesh*) mesh ) + void fillTransforms( shared mat4[] transforms, shared Bone bone, shared float time, shared mat4 parentTransform ) { - for( int i = 0; i < mesh.mNumBones; i++ ) + shared mat4 finalTransform; + if( bone.positionKeys.length == 0 && bone.rotationKeys.length == 0 && bone.scaleKeys.length == 0 ) { - if( name == cast(string)mesh.mBones[ i ].mName.data[ 0 .. mesh.mBones[ i ].mName.length ] ) - { - return i; - } + finalTransform = parentTransform * bone.nodeOffset; + transforms[ bone.boneNumber ] = finalTransform * bone.offset; } - - return -1; - } - - /** - * Check if string stringToTest ends with string end - * - * Params: - * - * Returns: - */ - bool checkEnd( string stringToTest, string end ) - { - if( stringToTest.length > end.length ) + else { - string temp = stringToTest[ (stringToTest.length - end.length) .. stringToTest.length ]; + shared mat4 boneTransform = mat4.identity; - if( stringToTest[ (stringToTest.length - end.length) .. stringToTest.length ] == end ) + if( bone.positionKeys.length > cast(int)time ) + { + boneTransform = boneTransform.translation( bone.positionKeys[ cast(int)time ].vector[ 0 ], bone.positionKeys[ cast(int)time ].vector[ 1 ], + bone.positionKeys[ cast(int)time ].vector[ 2 ] ); + } + if( bone.rotationKeys.length > cast(int)time ) { - return true; + boneTransform = boneTransform * bone.rotationKeys[ cast(int)time ].to_matrix!( 4, 4 ); } + if( bone.scaleKeys.length > cast(int)time ) + { + boneTransform = boneTransform.scale( bone.scaleKeys[ cast(int)time ].vector[ 0 ], bone.scaleKeys[ cast(int)time ].vector[ 1 ], bone.scaleKeys[ cast(int)time ].vector[ 2 ] ); + } + + finalTransform = parentTransform * boneTransform; + transforms[ bone.boneNumber ] = finalTransform * bone.offset; } - return false; + // Check children + for( int i = 0; i < bone.children.length; i++ ) + { + fillTransforms( transforms, bone.children[ i ], time, finalTransform ); + } } /** - * TODO + * Converts a aiVectorKey[] to vec3[]. * * Params: + * quaternions = aiVectorKey[] to be converted + * numKeys = Number of keys in vector array * - * Returns: - */ - shared( mat4[] ) getTransformsAtTime( shared float time ) - { - shared mat4[] boneTransforms = new shared mat4[ _numberOfBones ]; - - // Check shader/model - for( int i = 0; i < _numberOfBones; i++) + * Returns: The vectors in vector[] format + */ + shared( vec3[] ) convertVectorArray( const(aiVectorKey*) vectors, int numKeys ) { + shared vec3[] keys; + for( int i = 0; i < numKeys; i++ ) { - boneTransforms[ i ] = mat4.identity; + aiVector3D vector = vectors[ i ].mValue; + keys ~= vec3( vector.x, vector.y, vector.z ); } - fillTransforms( boneTransforms, _animationSet.animNodes, time, mat4.identity, 0 ); - - return boneTransforms; + return keys; } - /** - * TODO + * Converts a aiQuatKey[] to quat[]. * * Params: + * quaternions = aiQuatKey[] to be converted + * numKeys = Number of keys in quaternions array * - * Returns: + * Returns: The quaternions in quat[] format */ - void fillTransforms( shared mat4[] transforms, shared Node node, shared float time, shared mat4 parentTransform, int boneNum) + shared( quat[] ) convertQuat( const(aiQuatKey*) quaternions, int numKeys ) { - // Calculate matrix based on node.bone data and time - shared mat4 finalTransform; - shared mat4 boneTransform = mat4.identity; - // Data in the transform/scale partial nodes - shared mat4 test = mat4(0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, 32.7f, 0.0f, 0.0f, 1.0f, 0.02f, 0.0f, 0.0f, 0.0f, 1.0f); - shared mat4 test2 = mat4(0.978468f, 0.0f, -0.2064f, 12.2843f, 0.0f, 1.0f, 0.0f, 0.0f, 0.2064f, 0.0f, 0.978468f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f); - shared mat4 test3 = mat4(0.0f, -0.977f, 0.212f, 16.632f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.212f, 0.977f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f); - - if( node.positionKeys.length > cast(int)time ) - boneTransform = boneTransform * boneTransform.translation( node.positionKeys[ cast(int)time ].vector[ 0 ], node.positionKeys[ cast(int)time ].vector[ 1 ], node.positionKeys[ cast(int)time ].vector[ 2 ] ); - if( node.rotationKeys.length > cast(int)time ) - boneTransform = boneTransform * node.rotationKeys[ cast(int)time ].to_matrix!( 4, 4 ); - if( node.scaleKeys.length > cast(int)time ) - boneTransform.scale( node.scaleKeys[ cast(int)time ].vector[ 0 ], node.scaleKeys[ cast(int)time ].vector[ 1 ], node.scaleKeys[ cast(int)time ].vector[ 2 ] ); - - if(boneNum == 0) - { - finalTransform = (parentTransform * test) * boneTransform; - transforms[ node.id ] = finalTransform * node.transform; - } - if(boneNum == 1) - { - finalTransform = parentTransform * boneTransform; - transforms[ node.id ] = finalTransform * node.transform; - } - if(boneNum == 2) + shared quat[] keys; + for( int i = 0; i < numKeys; i++ ) { - finalTransform = (parentTransform) * boneTransform; - transforms[ node.id ] = finalTransform * node.transform; + aiQuatKey quaternion = quaternions[ i ]; + keys ~= quat( quaternion.mValue.w, quaternion.mValue.x, quaternion.mValue.y, quaternion.mValue.z ); } - boneNum++; - // Store the transform in the correct place and check children - for( int i = 0; i < node.children.length; i++ ) - { - fillTransforms( transforms, node.children[ i ], time, finalTransform, boneNum ); - } + return keys; } - /** - * TODO + * Converts an aiMatrix to a mat4 * * Params: + * aiMatrix = Matrix to be converted * - * Returns: + * Returns: The matrix in mat4 format */ mat4 convertAIMatrix( aiMatrix4x4 aiMatrix ) { @@ -399,11 +375,7 @@ public: } /** - * TODO - * - * Params: - * - * Returns: + * Shutdown the animation bone/pose data */ void shutdown() { @@ -411,33 +383,33 @@ public: } /** - * TODO + * A single animation track, storing its bones and poses */ shared struct AnimationSet { shared float duration; shared float fps; - shared Node animNodes; + shared Bone animBones; } - /** - * TODO + * A bone in the animation, storing everything it needs */ - shared class Node + shared class Bone { - this( shared string nodeName ) + this( string boneName, int boneNum ) { - name = nodeName; + name = boneName; + boneNumber = boneNum; } - shared string name; - shared int id; - shared Node parent; - shared Node[] children; + string name; + shared int boneNumber; + shared Bone[] children; shared vec3[] positionKeys; shared quat[] rotationKeys; shared vec3[] scaleKeys; - shared mat4 transform; + shared mat4 offset; + shared mat4 nodeOffset; } -} +} \ No newline at end of file diff --git a/source/components/assets.d b/source/components/assets.d index 7fe17b99..5bd39944 100644 --- a/source/components/assets.d +++ b/source/components/assets.d @@ -4,8 +4,7 @@ module components.assets; import components, utility; -import std.string; -import std.exception; +import std.string, std.array; import yaml; import derelict.freeimage.freeimage, derelict.assimp3.assimp; @@ -36,21 +35,33 @@ public: */ final shared(T) get( T )( string name ) if( is( T == Mesh ) || is( T == Texture ) || is( T == Material ) || is( T == AssetAnimation )) { + enum get( string array ) = q{ + if( auto result = name in $array ) + { + result.isUsed = true; + return *result; + } + else + { + logFatal( "Unable to find ", name, " in $array." ); + return null; + } + }.replace( "$array", array ); static if( is( T == Mesh ) ) { - return meshes[ name ]; + mixin( get!q{meshes} ); } else static if( is( T == Texture ) ) { - return textures[ name ]; + mixin( get!q{textures} ); } else static if( is( T == Material ) ) { - return materials[ name ]; + mixin( get!q{materials} ); } else static if( is( T == AssetAnimation ) ) { - return animations[ name ]; + mixin( get!q{animations} ); } else static assert( false, "Material of type " ~ T.stringof ~ " is not maintained by Assets." ); } @@ -81,17 +92,24 @@ public: aiProcess_CalcTangentSpace | aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_SortByPType ); //| aiProcess_FlipWindingOrder ); - enforce(scene, "Failed to load scene file '" ~ file.fullPath ~ "' Error: " ~ aiGetErrorString().fromStringz); + assert(scene, "Failed to load scene file '" ~ file.fullPath ~ "' Error: " ~ aiGetErrorString().fromStringz); // If animation data, add animation - if(scene.mNumAnimations > 0) - animations[ file.baseFileName ] = new shared AssetAnimation( scene.mAnimations[0], scene.mMeshes[0], scene.mRootNode ); - if( file.baseFileName in meshes ) logWarning( "Mesh ", file.baseFileName, " exsists more than once." ); // Add mesh - meshes[ file.baseFileName ] = new shared Mesh( file.fullPath, scene.mMeshes[0] ); + if( scene.mNumMeshes > 0 ) + { + if( scene.mNumAnimations > 0 ) + animations[ file.baseFileName ] = new shared AssetAnimation( scene.mAnimations[ 0 ], scene.mMeshes[ 0 ], scene.mRootNode ); + + meshes[ file.baseFileName ] = new shared Mesh( file.fullPath, scene.mMeshes[ 0 ] ); + } + else + { + logWarning( "Assimp did not contain mesh data, ensure you are loading a valid mesh." ); + } // Release mesh aiReleaseImport( scene ); @@ -100,7 +118,7 @@ public: foreach( file; FilePath.scanDirectory( FilePath.Resources.Textures ) ) { if( file.baseFileName in textures ) - logWarning( "Texture ", file.baseFileName, " exsists more than once." ); + logWarning( "Texture ", file.baseFileName, " exists more than once." ); textures[ file.baseFileName ] = new shared Texture( file.fullPath ); } @@ -110,7 +128,7 @@ public: auto name = object[ "Name" ].as!string; if( name in materials ) - logWarning( "Material ", name, " exsists more than once." ); + logWarning( "Material ", name, " exists more than once." ); materials[ name ] = Material.createFromYaml( object ); } @@ -126,29 +144,20 @@ public: */ final void shutdown() { - foreach_reverse( index; 0 .. meshes.length ) - { - auto name = meshes.keys[ index ]; - meshes[ name ].shutdown(); - meshes.remove( name ); - } - foreach_reverse( index; 0 .. textures.length ) - { - auto name = textures.keys[ index ]; - textures[ name ].shutdown(); - textures.remove( name ); - } - foreach_reverse( index; 0 .. materials.length ) - { - auto name = materials.keys[ index ]; - materials.remove( name ); - } - foreach_reverse( index; 0 .. animations.length ) - { - auto name = animations.keys[ index ]; - animations[ name ].shutdown(); - animations.remove( name ); - } + enum shutdownAA( string aaName, string friendlyName ) = q{ + foreach_reverse( name; meshes.keys ) + { + if( !$aaName[ name ].isUsed ) + logWarning( "$friendlyName ", name, " not used during this run." ); + + $aaName[ name ].shutdown(); + $aaName.remove( name ); + } + }.replaceMap( [ "$aaName": aaName, "$friendlyName": friendlyName ] ); + mixin( shutdownAA!( q{meshes}, "Mesh" ) ); + mixin( shutdownAA!( q{textures}, "Texture" ) ); + mixin( shutdownAA!( q{materials}, "Material" ) ); + mixin( shutdownAA!( q{animations}, "Animation" ) ); } } @@ -168,4 +177,4 @@ vn 0.0 0.0 1.0 f 4/3/1 3/4/1 1/2/1 f 2/1/1 4/3/1 1/2/1 -}; \ No newline at end of file +}; diff --git a/source/components/behavior.d b/source/components/behavior.d new file mode 100644 index 00000000..8ab7c19c --- /dev/null +++ b/source/components/behavior.d @@ -0,0 +1,184 @@ +/** + * Defines Behavior class, the base class for all scripts. + */ +module components.behavior; +import core, utility; + +import yaml; +import std.algorithm, std.array, std.traits; + +/** + * Defines methods for child classes to override. + */ +private abstract shared class ABehavior +{ + /// The object the behavior belongs to. + private GameObject _owner; + /// Function called by Behaviors to init the object. + /// Should not be touched by anything outside this module. + protected void initializeBehavior( Object param ) { } + /// The function called on initialization of the object. + void onInitialize() { } + /// Called on the update cycle. + void onUpdate() { } + /// Called on the draw cycle. + void onDraw() { } + /// Called on shutdown. + void onShutdown() { } +} + +/** + * Defines methods for child classes to override, with a parameter for onInitialize. + * + * Params: + * InitType = The type for onInitialize to take. + */ +abstract shared class Behavior( InitType = void ) : ABehavior +{ + static if( !is( InitType == void ) ) + { + InitType initArgs; + protected final override void initializeBehavior( Object param ) + { + initArgs = cast(shared InitType)param; + } + } + + /// Returns the GameObject which owns this behavior. + mixin( Getter!_owner ); + + /** + * Registers subclasses with onInit function pointers. + */ + shared static this() + { + static if( !is( InitType == void ) ) + { + foreach( mod; ModuleInfo ) + { + foreach( klass; mod.localClasses ) + { + if( klass.base == typeid(Behavior!InitType) ) + { + getInitParams[ klass.name ] = &Config.getObject!InitType; + } + } + } + } + } +} + +private shared Object function( Node )[string] getInitParams; + +/** + * Defines a collection of Behaviors to allow for multiple scripts to be added to an object. + */ +shared struct Behaviors +{ +private: + ABehavior[] behaviors; + shared GameObject _owner; + +public: + /** + * Constructor for Behaviors which assigns its owner. + * + * Params: + * owner = The owner of this behavior set. + */ + this( shared GameObject owner ) + { + _owner = owner; + } + + /** + * Adds a new behavior to the collection. + * + * Params: + * className = The behavior to add to the object. + * fields = Fields to convert give to behavior + */ + shared(ABehavior) createBehavior( string className, Node fields = Node( YAMLNull() ) ) + { + auto newBehavior = cast(shared ABehavior)Object.factory( className ); + + if( !newBehavior ) + { + logWarning( "Class ", className, " either not found or not child of Behavior." ); + return null; + } + + newBehavior._owner = _owner; + + if( !fields.isNull && className in getInitParams ) + { + newBehavior.initializeBehavior( getInitParams[ className ]( fields ) ); + } + + behaviors ~= newBehavior; + + return newBehavior; + } + + /** + * Adds a new behavior to the collection. + * + * Params: + * T = The behavior to add to the object. + * fields = Fields to convert give to behavior + */ + shared(T) createBehavior( T )( Node fields = Node( YAMLNull() ) ) if( is( T : ABehavior ) ) + { + auto newBehavior = new shared T; + + if( !newBehavior ) + { + logWarning( "Class ", T.stringof, " either not found or not child of Behavior." ); + return null; + } + + newBehavior._owner = _owner; + + if( !fields.isNull && T.stringof in getInitParams ) + { + newBehavior.initializeBehavior( getInitParams[ T.stringof ]( fields ) ); + } + + behaviors ~= newBehavior; + + return newBehavior; + } + + /** + * Gets the behavior of the given type. + * + * Returns: The bahavior + */ + auto get( BehaviorType = ABehavior )() + { + foreach( behav; behaviors ) + { + if( typeid(behav) == typeid(BehaviorType) ) + return cast(shared BehaviorType)behav; + } + + return null; + } + + mixin( callBehaviors ); +} + +enum callBehaviors = "".reduce!( ( a, func ) => a ~ + q{ + static if( __traits( compiles, ParameterTypeTuple!( __traits( getMember, ABehavior, "$func") ) ) && + ParameterTypeTuple!( __traits( getMember, ABehavior, "$func") ).length == 0 ) + { + void $func() + { + foreach( script; behaviors ) + { + script.$func(); + } + } + } + }.replace( "$func", func ) )( cast(string[])[__traits( derivedMembers, ABehavior )] ); diff --git a/source/components/camera.d b/source/components/camera.d index 77c25e51..e263de30 100644 --- a/source/components/camera.d +++ b/source/components/camera.d @@ -240,11 +240,11 @@ static this() obj.camera.owner = obj; //float fromYaml; - if( !Config.tryGet( "FOV", obj.camera._fov, yml ) ) + if( !yml.tryFind( "FOV", obj.camera._fov ) ) logFatal( obj.name, " is missing FOV value for its camera. "); - if( !Config.tryGet( "Near", obj.camera._near, yml ) ) + if( !yml.tryFind( "Near", obj.camera._near ) ) logFatal( obj.name, " is missing near plane value for its camera. "); - if( !Config.tryGet( "Far", obj.camera._far, yml ) ) + if( !yml.tryFind( "Far", obj.camera._far ) ) logFatal( obj.name, " is missing Far plane value for its camera. "); obj.camera.updatePerspective(); diff --git a/source/components/icomponent.d b/source/components/icomponent.d index 674b5b27..fd351bcd 100644 --- a/source/components/icomponent.d +++ b/source/components/icomponent.d @@ -1,5 +1,5 @@ /** - * Defines the Component abstract class, which is the base for all components. + * Defines the IComponent interface, which is the base for all components. */ module components.icomponent; import core, graphics; diff --git a/source/components/lights.d b/source/components/lights.d index ee845362..2c7087f6 100644 --- a/source/components/lights.d +++ b/source/components/lights.d @@ -23,6 +23,16 @@ public: { this.color = color; } + + static this() + { + IComponent.initializers[ "Light" ] = ( Node yml, shared GameObject obj ) + { + obj.light = cast(shared)yml.get!Light; + obj.light.owner = obj; + return obj.light; + }; + } override void update() { } override void shutdown() { } @@ -116,15 +126,3 @@ public: super( color ); } } - -static this() -{ - import yaml; - IComponent.initializers[ "Light" ] = ( Node yml, shared GameObject obj ) - { - obj.light = cast(shared)yml.get!Light; - obj.light.owner = obj; - - return obj.light; - }; -} diff --git a/source/components/material.d b/source/components/material.d index ff4949aa..f8a7e65f 100644 --- a/source/components/material.d +++ b/source/components/material.d @@ -15,6 +15,7 @@ shared final class Material { private: Texture _diffuse, _normal, _specular; + bool _isUsed; public: /// The diffuse (or color) map. @@ -23,13 +24,16 @@ public: mixin( Property!(_normal, AccessModifier.Public) ); /// The specular map, which specifies how shiny a given point is. mixin( Property!(_specular, AccessModifier.Public) ); + /// Whether or not the material is actually used. + mixin( Property!( _isUsed, AccessModifier.Package ) ); /** * Default constructor, makes sure everything is initialized to default. */ this() { - _diffuse = _normal = _specular = defaultTex; + _diffuse = _specular = defaultTex; + _normal = defaultNormal; } /** @@ -45,17 +49,25 @@ public: auto obj = new shared Material; string prop; - if( Config.tryGet( "Diffuse", prop, yamlObj ) ) + if( yamlObj.tryFind( "Diffuse", prop ) ) obj.diffuse = Assets.get!Texture( prop ); - if( Config.tryGet( "Normal", prop, yamlObj ) ) + if( yamlObj.tryFind( "Normal", prop ) ) obj.normal = Assets.get!Texture( prop ); - if( Config.tryGet( "Specular", prop, yamlObj ) ) + if( yamlObj.tryFind( "Specular", prop ) ) obj.specular = Assets.get!Texture( prop ); return obj; } + + /** + * Shuts down the material, making sure all references are released. + */ + void shutdown() + { + _diffuse = _specular = _normal = null; + } } /** @@ -64,7 +76,10 @@ public: shared class Texture { protected: - uint _width, _height, _glID; + uint _width = 1; + uint _height = 1; + uint _glID; + bool _isUsed; /** * TODO @@ -106,6 +121,8 @@ public: mixin( Property!_height ); /// TODO mixin( Property!_glID ); + /// Whether or not the texture is actually used. + mixin( Property!( _isUsed, AccessModifier.Package ) ); /** * TODO @@ -142,18 +159,27 @@ public: } /** - * TODO - * - * Params: - * - * Returns: + * A default black texture. */ @property shared(Texture) defaultTex() { static shared Texture def; if( !def ) - def = new shared Texture( [0, 0, 0, 255] ); + def = new shared Texture( [cast(ubyte)0, cast(ubyte)0, cast(ubyte)0, cast(ubyte)255].ptr ); + + return def; +} + +/** + * A default gray texture + */ +@property shared(Texture) defaultNormal() +{ + static shared Texture def; + + if( !def ) + def = new shared Texture( [cast(ubyte)255, cast(ubyte)127, cast(ubyte)127, cast(ubyte)255].ptr ); return def; } diff --git a/source/components/mesh.d b/source/components/mesh.d index 0501f944..2b0c7a68 100644 --- a/source/components/mesh.d +++ b/source/components/mesh.d @@ -5,7 +5,7 @@ module components.mesh; import core, components, graphics, utility; import derelict.opengl3.gl3, derelict.assimp3.assimp; -import gl3n.linalg; +import gl3n.linalg, gl3n.aabb; import std.stdio, std.stream, std.format, std.math; @@ -53,6 +53,8 @@ shared class Mesh : IComponent private: uint _glVertexArray, _numVertices, _numIndices, _glIndexBuffer, _glVertexBuffer; bool _animated; + bool _isUsed; + AABB _boundingBox; public: /// TODO @@ -67,6 +69,10 @@ public: mixin( Property!_glVertexBuffer ); /// TODO mixin( Property!_animated ); + /// The bounding box of the mesh. + mixin( RefGetter!_boundingBox ); + /// Whether or not the material is actually used. + mixin( Property!( _isUsed, AccessModifier.Package ) ); /** * Creates a mesh. @@ -81,6 +87,7 @@ public: float[] outputData; uint[] indices; animated = false; + boundingBox = AABB.from_points( [] ); if( mesh ) { // If there is animation data @@ -159,6 +166,9 @@ public: //outputData ~= bitangent.z; outputData ~= vertBones[ face.mIndices[ j ] ][0..4]; outputData ~= vertWeights[ face.mIndices[ j ] ][0..4]; + + // Save the position in verts + boundingBox.expand( shared vec3( pos.x, pos.y, pos.z ) ); } } } @@ -199,6 +209,9 @@ public: //outputData ~= bitangent.x; //outputData ~= bitangent.y; //outputData ~= bitangent.z; + + // Save the position in verts + boundingBox.expand( shared vec3( pos.x, pos.y, pos.z ) ); } } } diff --git a/source/components/package.d b/source/components/package.d index 6d624903..cee51b79 100644 --- a/source/components/package.d +++ b/source/components/package.d @@ -8,3 +8,4 @@ import components.mesh; import components.camera; import components.lights; import components.userinterface; +import components.behavior; diff --git a/source/components/userinterface.d b/source/components/userinterface.d index 3009047f..3ea4bc07 100644 --- a/source/components/userinterface.d +++ b/source/components/userinterface.d @@ -1,5 +1,5 @@ /** - * Handles the creation and life cycle of a web view + * Handles the creation and life cycle of UI objects and webview textures */ module components.userinterface; import core; @@ -7,7 +7,7 @@ import utility.awesomium, components, utility, graphics.graphics; import std.string, gl3n.linalg; /** - * TODO + * User interface objects handle drawing/updating an AwesomiumView over the screen */ shared class UserInterface { @@ -19,11 +19,19 @@ private: // TODO: Handle JS public: - /// TODO + /// WebView to be drawn mixin( Property!(_view, AccessModifier.Public) ); - /// TODO + /// Scale of the UI mixin( Property!(_scaleMat, AccessModifier.Public) ); + /** + * Create UI object + * + * Params: + * w = Width (in pixels) of UI + * h = Height (in pixels) of UI + * filePath = Absolute file path to UI file + */ this( uint w, uint h, string filePath ) { _scaleMat = mat4.identity; @@ -35,30 +43,46 @@ public: logDebug( "UI File: ", filePath ); } + /** + * Update UI view + */ void update() { - // Check for mouse & keyboard input + /// TODO: Check for mouse & keyboard input _view.update(); return; } + /** + * Draw UI view + */ void draw() { Graphics.addUI( this ); } + /** + * Cleanup UI memory + */ void shutdown() { + // Try to clean up gl buffers + _view.shutdown(); // Clean up mesh, material, and view } + /* void keyPress(int key) { } + */ + /** + * Initializes Awesomium singleton + */ static void initializeAwesomium() { version( Windows ) @@ -72,12 +96,18 @@ public: } } + /** + * Updates Awesomium singleton + */ static void updateAwesomium() { version( Windows ) awe_webcore_update(); } + /** + * Shutdowns Awesomium singleton + */ static void shutdownAwesomium() { version( Windows ) @@ -85,6 +115,10 @@ public: } } + +/** + * Creates an Awesomium web view texture + */ shared class AwesomiumView : Texture, IComponent { private: @@ -150,6 +184,7 @@ public: override void shutdown() { + destroy( glBuffer ); version( Windows ) awe_webview_destroy( cast(awe_webview*)webView ); } diff --git a/source/core/dgame.d b/source/core/dgame.d index ff61dfbe..c0d16176 100644 --- a/source/core/dgame.d +++ b/source/core/dgame.d @@ -182,7 +182,6 @@ private: bench!( { Config.initialize(); } )( "Config init" ); bench!( { Logger.initialize(); } )( "Logger init" ); bench!( { Input.initialize(); } )( "Input init" ); - bench!( { Output.initialize(); } )( "Output init" ); bench!( { Graphics.initialize(); } )( "Graphics init" ); bench!( { Assets.initialize(); } )( "Assets init" ); bench!( { Prefabs.initialize(); } )( "Prefabs init" ); diff --git a/source/core/gameobject.d b/source/core/gameobject.d index 53aaaf55..c8818b83 100644 --- a/source/core/gameobject.d +++ b/source/core/gameobject.d @@ -7,7 +7,7 @@ import core, components, graphics, utility; import yaml; import gl3n.linalg, gl3n.math; -import std.conv, std.variant; +import std.conv, std.variant, std.array, std.typecons; enum AnonymousName = "__anonymous"; @@ -45,7 +45,7 @@ shared struct ObjectStateFlags /** * Manages all components and transform in the world. Can be overridden. */ -shared class GameObject +shared final class GameObject { private: Transform _transform; @@ -59,6 +59,8 @@ private: IComponent[TypeInfo] componentList; string _name; ObjectStateFlags* _stateFlags; + bool canChangeName; + Behaviors _behaviors; static uint nextId = 1; package: @@ -66,7 +68,7 @@ package: public: /// The current transform of the object. - mixin( Property!( _transform, AccessModifier.Public ) ); + mixin( RefGetter!( _transform, AccessModifier.Public ) ); /// The Material belonging to the object. mixin( Property!( _material, AccessModifier.Public ) ); /// The Mesh belonging to the object. @@ -81,10 +83,14 @@ public: mixin( Property!( _parent, AccessModifier.Public ) ); /// All of the objects which list this as parent mixin( Property!( _children, AccessModifier.Public ) ); - /// The name of the object. - mixin( Property!( _name, AccessModifier.Public ) ); /// The current update settings mixin( Property!( _stateFlags, AccessModifier.Public ) ); + /// The name of the object. + mixin( Getter!_name ); + /// The scripts this object owns. + mixin( RefGetter!_behaviors ); + /// ditto + mixin( ConditionalSetter!( _name, q{canChangeName}, AccessModifier.Public ) ); /// The ID of the object immutable uint id; @@ -93,77 +99,67 @@ public: * * Params: * yamlObj = The YAML node to pull info from. - * scriptOverride = The ClassInfo to use to create the object. Overrides YAML setting. * * Returns: * A new game object with components and info pulled from yaml. */ - static shared(GameObject) createFromYaml( Node yamlObj, const ClassInfo scriptOverride = null ) + static shared(GameObject) createFromYaml( Node yamlObj ) { shared GameObject obj; bool foundClassName; string prop, className; Node innerNode; - string objName = yamlObj[ "Name" ].as!string; - - // Try to get from script - if( scriptOverride !is null ) + if( yamlObj.tryFind( "InstanceOf", prop ) ) { - obj = cast(shared GameObject)scriptOverride.create(); + obj = Prefabs[ prop ].createInstance(); } else { - foundClassName = Config.tryGet( "Script.ClassName", className, yamlObj ); - // Get class to create script from - const ClassInfo scriptClass = foundClassName - ? ClassInfo.find( className ) - : null; - - // Check that if a Script.ClassName was provided that it was valid - if( foundClassName && scriptClass is null ) - { - logWarning( objName, ": Unable to find Script ClassName: ", className ); - } - - if( Config.tryGet( "InstanceOf", prop, yamlObj ) ) - { - obj = Prefabs[ prop ].createInstance( scriptClass ); - } - else - { - obj = scriptClass - ? cast(shared GameObject)scriptClass.create() - : new shared GameObject; - } + obj = new shared GameObject; } - // set object name - obj.name = objName; + // Set object name + obj.name = yamlObj[ "Name" ].as!string; // Init transform - if( Config.tryGet( "Transform", innerNode, yamlObj ) ) + if( yamlObj.tryFind( "Transform", innerNode ) ) { shared vec3 transVec; - if( Config.tryGet( "Scale", transVec, innerNode ) ) + if( innerNode.tryFind( "Scale", transVec ) ) obj.transform.scale = shared vec3( transVec ); - if( Config.tryGet( "Position", transVec, innerNode ) ) + if( innerNode.tryFind( "Position", transVec ) ) obj.transform.position = shared vec3( transVec ); - if( Config.tryGet( "Rotation", transVec, innerNode ) ) + if( innerNode.tryFind( "Rotation", transVec ) ) obj.transform.rotation = quat.identity.rotatex( transVec.x.radians ).rotatey( transVec.y.radians ).rotatez( transVec.z.radians ); } - if( foundClassName && Config.tryGet( "Script.Fields", innerNode, yamlObj ) ) + if( yamlObj.tryFind( "Behaviors", innerNode ) ) { - if( auto initParams = className in getInitParams ) - obj.initialize( (*initParams)( innerNode ) ); + if( !innerNode.isSequence ) + { + logWarning( "Behaviors tag of ", obj.name, " must be a sequence." ); + } + else + { + foreach( Node behavior; innerNode ) + { + string className; + Node fields; + if( !behavior.tryFind( "Class", className ) ) + logFatal( "Behavior element in ", obj.name, " must have a Class value." ); + if( !behavior.tryFind( "Fields", fields ) ) + fields = Node( YAMLNull() ); + obj.behaviors.createBehavior( className, fields ); + } + } } // If parent is specified, add it to the map - if( Config.tryGet( "Parent", prop, yamlObj ) ) + if( yamlObj.tryFind( "Parent", prop ) ) logWarning( "Specifying parent objects by name is deprecated. Please add this as an inline child to ", prop, "." ); - if( Config.tryGet( "Children", innerNode, yamlObj ) ) + if( yamlObj.tryFind( "Children", innerNode ) ) { if( innerNode.isSequence ) { @@ -172,7 +168,7 @@ public: if( child.isScalar ) { // Add child name to map. - logWarning( "Specifing child objects by name is deprecated. Please add ", child.get!string, " as an inline child of ", objName, "." ); + logWarning( "Specifing child objects by name is deprecated. Please add ", child.get!string, " as an inline child of ", obj.name, "." ); } else { @@ -190,7 +186,7 @@ public: // Init components foreach( string key, Node value; yamlObj ) { - if( key == "Name" || key == "Script" || key == "Parent" || key == "InstanceOf" || key == "Transform" || key == "Children" ) + if( key == "Name" || key == "Script" || key == "Parent" || key == "InstanceOf" || key == "Transform" || key == "Children" || key == "Behaviors" ) continue; if( auto init = key in IComponent.initializers ) @@ -199,15 +195,35 @@ public: logWarning( "Unknown key: ", key ); } + obj.behaviors.onInitialize(); + return obj; } + /** + * Create a GameObject from a Yaml node. + * + * Params: + * fields = The YAML node to pull info from. + * + * Returns: + * A tuple of the object created at index 0, and the behavior at index 1. + */ + static auto createWithBehavior( BehaviorT )( Node fields = Node( YAMLNull() ) ) + { + auto newObj = new shared GameObject; + + newObj.behaviors.createBehavior!BehaviorT( fields ); + + return tuple( newObj, newObj.behaviors.get!BehaviorT ); + } /** * Creates basic GameObject with transform and connection to transform's emitter. */ this() { - transform = new shared Transform( this ); + _transform = shared Transform( this ); + _behaviors = shared Behaviors( this ); // Create default material material = new shared Material(); @@ -215,11 +231,9 @@ public: stateFlags = new ObjectStateFlags; stateFlags.resumeAll(); - } - ~this() - { - destroy( transform ); + name = typeid(this).name.split( '.' )[ $-1 ] ~ id.to!string; + canChangeName = true; } /** @@ -229,7 +243,7 @@ public: { if( stateFlags.update ) { - onUpdate(); + behaviors.onUpdate(); foreach( ci, component; componentList ) component.update(); @@ -245,7 +259,7 @@ public: */ final void draw() { - onDraw(); + behaviors.onDraw(); foreach( obj; children ) obj.draw(); @@ -256,7 +270,7 @@ public: */ final void shutdown() { - onShutdown(); + behaviors.onShutdown(); foreach( obj; children ) obj.shutdown(); @@ -294,26 +308,17 @@ public: */ final void addChild( shared GameObject newChild ) { - import std.algorithm; // Nothing to see here. if( cast()newChild.parent == cast()this ) return; // Remove from current parent - else if( newChild.parent && cast()newChild.parent != cast()this ) - { - // Get index of object being removed - auto newChildIndex = (cast(GameObject[])newChild.parent.children).countUntil( cast()newChild ); - // Get objects after one being removed - auto end = newChild.parent.children[ newChildIndex+1..$ ]; - // Get objects before one being removed - newChild.parent.children = newChild.parent.children[ 0..newChildIndex ]; - // Add end back - newChild.parent._children ~= end; - } + else if( newChild.parent ) + newChild.parent.removeChild( newChild ); _children ~= newChild; newChild.parent = this; - + newChild.canChangeName = false; + // Get root object shared GameObject par; for( par = this; par.parent; par = par.parent ) { } @@ -341,58 +346,23 @@ public: { par.scene.objectById[ child.id ] = child; par.scene.idByName[ child.name ] = child.id; - } + } } - - } - - /// Called on the update cycle. - void onUpdate() { } - /// Called on the draw cycle. - void onDraw() { } - /// Called on shutdown. - void onShutdown() { } - /// Called when the object collides with another object. - void onCollision( GameObject other ) { } - - /// Allows for GameObjectInit to pass o to typed func. - void initialize( Object o ) { } -} - -private shared Object function( Node )[string] getInitParams; -/** - * Class to extend when looking to use the onInitialize function. - * - * Type Params: - * T = The type onInitialize will recieve. - */ -class GameObjectInit(T) : GameObject if( is( T == class ) ) -{ - /// Function to override to get args from Fields field in YAML. - abstract void onInitialize( T args ); - - /// Overridden to give params to child class. - final override void initialize( Object o ) - { - onInitialize( cast(T)o ); } /** - * Registers subclasses with onInit function pointers/ + * Removes the given object as a child from this object. + * + * Params: + * oldChild = The object to remove. */ - shared static this() + final void removeChild( shared GameObject oldChild ) { - foreach( mod; ModuleInfo ) - { - foreach( klass; mod.localClasses ) - { - if( klass.base == typeid(GameObjectInit!T) ) - { - getInitParams[ klass.name ] = &Config.getObject!T; - } - } - } + children = children.remove( oldChild ); + + oldChild.canChangeName = true; + oldChild.parent = null; } } @@ -402,7 +372,7 @@ class GameObjectInit(T) : GameObject if( is( T == class ) ) * and can generate a World matrix, worldPosition/Rotation (based on parents' transforms) * as well as forward, up, and right axes based on rotation */ -final shared class Transform : IDirtyable +private shared struct Transform { private: GameObject _owner; @@ -411,28 +381,13 @@ private: vec3 _prevScale; mat4 _matrix; -public: - // these should remain public fields, properties return copies not references - /// TODO - vec3 position; - /// TODO - quat rotation; - /// TODO - vec3 scale; - - /// TODO - mixin( Property!( _owner, AccessModifier.Public ) ); - /// TODO - mixin( ThisDirtyGetter!( _matrix, updateMatrix ) ); - /** - * TODO + * Default constructor, most often created by GameObjects. * * Params: - * - * Returns: + * obj = The object the transform belongs to. */ - this( shared GameObject obj = null ) + this( shared GameObject obj ) { owner = obj; position = vec3(0,0,0); @@ -440,13 +395,28 @@ public: rotation = quat.identity; } - ~this() - { - } +public: + // these should remain public fields, properties return copies not references + /// The position of the object in local space. + vec3 position; + /// The rotation of the object in local space. + quat rotation; + /// The absolute scale of the object. Ignores parent scale. + vec3 scale; + + /// The object which this belongs to. + mixin( Property!( _owner, AccessModifier.Public ) ); + /// The world matrix of the transform. + mixin( Getter!_matrix ); + //mixin( ThisDirtyGetter!( _matrix, updateMatrix ) ); + + @disable this(); /** - * This returns the object's position relative to the world origin, not the parent - */ + * This returns the object's position relative to the world origin, not the parent. + * + * Returns: The object's position relative to the world origin, not the parent. + */ final @property shared(vec3) worldPosition() @safe pure nothrow { if( owner.parent is null ) @@ -456,8 +426,10 @@ public: } /** - * This returns the object's rotation relative to the world origin, not the parent - */ + * This returns the object's rotation relative to the world origin, not the parent. + * + * Returns: The object's rotation relative to the world origin, not the parent. + */ final @property shared(quat) worldRotation() @safe pure nothrow { if( owner.parent is null ) @@ -469,8 +441,10 @@ public: /* * Check if current or a parent's matrix needs to be updated. * Called automatically when getting matrix. + * + * Returns: Whether or not the object is dirty. */ - final override @property bool isDirty() @safe pure nothrow + final @property bool isDirty() @safe pure nothrow { bool result = position != _prevPos || rotation != _prevRot || @@ -480,9 +454,9 @@ public: } /* - * Gets the forward axis of the current transform + * Gets the forward axis of the current transform. * - * Returns: The forward axis of the current transform + * Returns: The forward axis of the current transform. */ final @property const shared(vec3) forward() { @@ -497,16 +471,16 @@ public: import gl3n.math; writeln( "Dash Transform forward unittest" ); - auto trans = new shared Transform(); + auto trans = new shared Transform( null ); auto forward = shared vec3( 0.0f, 1.0f, 0.0f ); trans.rotation.rotatex( 90.radians ); assert( almost_equal( trans.forward, forward ) ); } /* - * Gets the up axis of the current transform + * Gets the up axis of the current transform. * - * Returns: The up axis of the current transform + * Returns: The up axis of the current transform. */ final @property const shared(vec3) up() { @@ -521,17 +495,17 @@ public: import gl3n.math; writeln( "Dash Transform up unittest" ); - auto trans = new shared Transform(); + auto trans = new shared Transform( null ); auto up = shared vec3( 0.0f, 0.0f, 1.0f ); trans.rotation.rotatex( 90.radians ); assert( almost_equal( trans.up, up ) ); } - + /* - * Gets the right axis of the current transform + * Gets the right axis of the current transform. * - * Returns: The right axis of the current transform + * Returns: The right axis of the current transform. */ final @property const shared(vec3) right() { @@ -546,7 +520,7 @@ public: import gl3n.math; writeln( "Dash Transform right unittest" ); - auto trans = new shared Transform(); + auto trans = new shared Transform( null ); auto right = shared vec3( 0.0f, 0.0f, -1.0f ); trans.rotation.rotatey( 90.radians ); @@ -554,7 +528,7 @@ public: } /** - * Rebuilds the object's matrix + * Rebuilds the object's matrix. */ final void updateMatrix() @safe pure nothrow { diff --git a/source/core/prefabs.d b/source/core/prefabs.d index f96a244d..16b9faaf 100644 --- a/source/core/prefabs.d +++ b/source/core/prefabs.d @@ -70,9 +70,9 @@ public: * Returns: * The new GameObject from the Prefab. */ - final shared(GameObject) createInstance( const ClassInfo scriptOverride = null ) + final shared(GameObject) createInstance() { - return GameObject.createFromYaml( yaml, scriptOverride ); + return GameObject.createFromYaml( yaml ); } private: diff --git a/source/core/properties.d b/source/core/properties.d index ad7c4f65..a642f44e 100644 --- a/source/core/properties.d +++ b/source/core/properties.d @@ -4,6 +4,7 @@ * Authors: Colden Cullen, ColdenCullen@gmail.com */ module core.properties; +import utility.string; public import std.traits; import std.array; @@ -13,6 +14,7 @@ enum AccessModifier : string Public = "public", Protected = "protected", Private = "private", + Package = "package", } /** @@ -49,6 +51,26 @@ template Getter( alias field, AccessModifier access = AccessModifier.Public, str "$access": cast(string)access ] ); } +/** + * Generates a getter for a field that returns a reference to it. + * + * Params: + * field = The field to generate the property for. + * access = The access modifier for the getter function. + * name = The name of the property functions. Defaults to the field name minus the first character. Meant for fields that start with underscores. + */ +template RefGetter( alias field, AccessModifier access = AccessModifier.Public, string name = field.stringof[ 1..$ ] ) +{ + enum RefGetter = q{ + final $access @property auto ref $name() @safe pure nothrow + { + return $field; + }} + .replaceMap( [ + "$field": field.stringof, "$name": name, + "$access": cast(string)access ] ); +} + /** * Generates a getter for a field that can be marked as dirty. Calls updateFunc if is dirty. * @@ -128,14 +150,30 @@ template ThisDirtyGetter( alias field, alias updateFunc, AccessModifier access = */ template Setter( alias field, AccessModifier access = AccessModifier.Protected, string name = field.stringof[ 1..$ ] ) { - enum Setter = q{ + enum Setter = ConditionalSetter!( field, q{true}, access, name ); +} + +/** + * Generates a setter for a field, that only sets if a condition is met. + * + * Params: + * field = The field to generate the property for. + * condition = The condition to evaluate when assigning. + * access = The access modifier for the setter function. + * name = The name of the property functions. Defaults to the field name minus the first character. Meant for fields that start with underscores. + */ +template ConditionalSetter( alias field, string condition, AccessModifier access = AccessModifier.Protected, string name = field.stringof[ 1..$ ] ) +{ + enum ConditionalSetter = q{ final $access @property void $name( $type newVal ) @safe pure nothrow { - $field = newVal; + if( $condition ) + $field = newVal; }} .replaceMap( [ "$field": field.stringof, "$access": cast(string)access, - "$name": name, "$type": typeof(field).stringof ] ); + "$name": name, "$type": typeof(field).stringof, + "$condition": condition ] ); } /** @@ -147,18 +185,6 @@ shared interface IDirtyable } private: -T replaceMap( T, TKey, TValue )( T base, TKey[TValue] replaceMap ) if( isSomeString!T && isSomeString!TKey && isSomeString!TValue ) -{ - auto result = base; - - foreach( key, value; replaceMap ) - { - result = result.replace( key, value ); - } - - return result; -} - string functionTraitsString( alias func )() { string result = ""; diff --git a/source/core/scene.d b/source/core/scene.d index d66c72d5..7f3ca05a 100644 --- a/source/core/scene.d +++ b/source/core/scene.d @@ -1,6 +1,6 @@ /** * This module defines the Scene class, TODO - * + * */ module core.scene; import core, components, graphics, utility; @@ -10,13 +10,13 @@ import std.path; enum SceneName = "[scene]"; /** - * TODO + * The Scene contains a list of all objects that should be drawn at a given time. */ shared final class Scene { private: - GameObject root; - + GameObject _root; + package: GameObject[uint] objectById; uint[string] idByName; @@ -24,17 +24,19 @@ package: public: /// The camera to render with. Camera camera; + /// The root object of the scene. + mixin( Getter!_root ); this() { - root = new shared GameObject; - root.name = SceneName; - root.scene = this; + _root = new shared GameObject; + _root.name = SceneName; + _root.scene = this; } /** * Load all objects inside the specified folder in FilePath.Objects. - * + * * Params: * objectPath = The folder location inside of /Objects to look for objects in. */ @@ -43,7 +45,7 @@ public: foreach( yml; loadYamlDocuments( buildNormalizedPath( FilePath.Resources.Objects, objectPath ) ) ) { // Create the object - root.addChild( GameObject.createFromYaml( yml ) ); + _root.addChild( GameObject.createFromYaml( yml ) ); } } @@ -52,28 +54,33 @@ public: */ final void clear() { - root = new shared GameObject; + _root = new shared GameObject; } /** - * TODO - */ + * Updates all objects in the scene. + */ final void update() { - root.update(); + _root.update(); } /** - * TODO - */ + * Draws all objects in the scene. + */ final void draw() { - root.draw(); + _root.draw(); } /** - * TODO - */ + * Gets the object in the scene with the given name. + * + * Params: + * name = The name of the object to look for. + * + * Returns: The object with the given name. + */ final shared(GameObject) opIndex( string name ) { if( auto id = name in idByName ) @@ -83,8 +90,13 @@ public: } /** - * TODO - */ + * Gets the object in the scene with the given id. + * + * Params: + * index = The id of the object to look for. + * + * Returns: The object with the given id. + */ final shared(GameObject) opIndex( uint index ) { if( auto obj = index in objectById ) @@ -101,12 +113,25 @@ public: */ final void addChild( shared GameObject newChild ) { - root.addChild( newChild ); + _root.addChild( newChild ); + } + + /** + * Removes the given object as a child from this scene. + * + * Params: + * oldChild = The object to remove. + */ + final void removechild( shared GameObject oldChild ) + { + _root.removeChild( oldChild ); } /** - * TODO - */ + * Gets all objects in the scene. + * + * Returns: All objects belonging to this scene. + */ final @property shared(GameObject[]) objects() { return objectById.values; diff --git a/source/graphics/adapters/adapter.d b/source/graphics/adapters/adapter.d index 250df106..f4e6920d 100644 --- a/source/graphics/adapters/adapter.d +++ b/source/graphics/adapters/adapter.d @@ -4,7 +4,7 @@ module graphics.adapters.adapter; import core, components, graphics, utility; -import gl3n.linalg; +import gl3n.linalg, gl3n.frustum; import derelict.opengl3.gl3; import std.algorithm, std.array; @@ -215,10 +215,27 @@ public: */ void geometryPass() { + void updateMatricies( shared GameObject current ) + { + current.transform.updateMatrix(); + foreach( child; current.children ) + updateMatricies( child ); + } + updateMatricies( scene.root ); + foreach( object; scene.objects ) { if( object.mesh && object.stateFlags.drawMesh ) { + shared mat4 worldView = scene.camera.viewMatrix * object.transform.matrix; + shared mat4 worldViewProj = projection * worldView; + + if( !( object.mesh.boundingBox in shared Frustum( worldViewProj ) ) ) + { + // If we can't see an object, don't draw it. + continue; + } + // set the shader Shader shader = object.mesh.animated ? Shaders.animatedGeometry @@ -226,11 +243,8 @@ public: glUseProgram( shader.programID ); glBindVertexArray( object.mesh.glVertexArray ); - - shared mat4 worldView = scene.camera.viewMatrix * object.transform.matrix; shader.bindUniformMatrix4fv( shader.WorldView, worldView ); - shader.bindUniformMatrix4fv( shader.WorldViewProjection, - projection * worldView ); + shader.bindUniformMatrix4fv( shader.WorldViewProjection, worldViewProj ); shader.bindUniform1ui( shader.ObjectId, object.id ); if( object.mesh.animated ) @@ -420,7 +434,7 @@ protected: */ final void loadProperties() { - fullscreen = Config.get!bool( "Display.Fullscreen" ); + fullscreen = config.find!bool( "Display.Fullscreen" ); if( fullscreen ) { width = screenWidth; @@ -428,11 +442,11 @@ protected: } else { - width = Config.get!uint( "Display.Width" ); - height = Config.get!uint( "Display.Height" ); + width = config.find!uint( "Display.Width" ); + height = config.find!uint( "Display.Height" ); } - backfaceCulling = Config.get!bool( "Graphics.BackfaceCulling" ); - vsync = Config.get!bool( "Graphics.VSync" ); + backfaceCulling = config.find!bool( "Graphics.BackfaceCulling" ); + vsync = config.find!bool( "Graphics.VSync" ); } } diff --git a/source/utility/array.d b/source/utility/array.d new file mode 100644 index 00000000..10263859 --- /dev/null +++ b/source/utility/array.d @@ -0,0 +1,30 @@ +module utility.array; + +/** + * Removes an element from a shared array. + * + * Params: + * haystack = The array to remove from. + * needle = The element to remove. + * + * Returns: A copy of haystack without needle in it. + */ +shared(T[]) remove(T)( shared T[] haystack, shared T needle ) +{ + import std.algorithm, core.memory; + // Get index of object being removed + auto needleIndex = (cast(T[])haystack).countUntil( cast(T)needle ); + + // Return if not actually a child + if( needleIndex == -1 ) + return haystack; + + auto result = cast(shared T*)GC.malloc( T.sizeof * ( haystack.length - 1 ) ); + + // Add beginning of list + result[ 0..needleIndex ] = haystack[ 0..needleIndex ]; + // Add end of list + result[ needleIndex..haystack.length - 1 ] = haystack[ needleIndex+1..$ ]; + + return result[ 0..haystack.length - 1 ]; +} diff --git a/source/utility/concurrency.d b/source/utility/concurrency.d index e65056db..fa5ea351 100644 --- a/source/utility/concurrency.d +++ b/source/utility/concurrency.d @@ -1,5 +1,5 @@ /** - * TODO + * Defines some useful helpers for running the engine concurrently. */ module utility.concurrency; diff --git a/source/utility/config.d b/source/utility/config.d index 85b96fc9..d2df640d 100644 --- a/source/utility/config.d +++ b/source/utility/config.d @@ -2,28 +2,15 @@ * Defines the static class Config, which handles all configuration options. */ module utility.config; -import utility.filepath; +import utility.filepath, utility.output; -// Imports for conversions -import components.assets, components.lights; -import graphics.shaders; -import utility.output : Verbosity; -import utility.input : Keyboard; -import utility; +public import yaml; -import gl3n.linalg; -import yaml; +import std.variant, std.algorithm, std.traits, + std.array, std.conv, std.file; -import std.array, std.conv, std.string, std.path, - std.typecons, std.variant, std.parallelism, - std.traits, std.algorithm, std.file; - -private Node contentNode; private string fileToYaml( string filePath ) { return filePath.replace( "\\", "/" ).replace( "../", "" ).replace( "/", "." ); } -version( EmbedContent ) -string contentYML; - /** * Place this mixin anywhere in your game code to allow the Content.yml file * to be imported at compile time. Note that this will only actually import @@ -38,10 +25,95 @@ mixin template ContentImport() import utility.config; contentYML = import( "Content.yml" ); } - } } +/// The node config values are stored. +Node config; +/// The constructor used to load load new yaml files. +Constructor constructor; +/// The string to store imported yaml content in. +version( EmbedContent ) string contentYML; +private Node contentNode; + +/// Initializes contentNode and constructor. +static this() +{ + constructor = new Constructor; + contentNode = Node( YAMLNull() ); + + import components.lights; + import graphics.shaders.shaders; + import utility.input, utility.output; + import gl3n.linalg; + + constructor.addConstructorScalar( "!Vector2", ( ref Node node ) + { + shared vec2 result; + string[] vals = node.as!string.split(); + if( vals.length != 2 ) + throw new Exception( "Invalid number of values: " ~ node.as!string ); + + result.x = vals[ 0 ].to!float; + result.y = vals[ 1 ].to!float; + return result; + } ); + constructor.addConstructorScalar( "!Vector3", ( ref Node node ) + { + shared vec3 result; + string[] vals = node.as!string.split(); + if( vals.length != 3 ) + throw new Exception( "Invalid number of values: " ~ node.as!string ); + + result.x = vals[ 0 ].to!float; + result.y = vals[ 1 ].to!float; + result.z = vals[ 2 ].to!float; + return result; + } ); + constructor.addConstructorScalar( "!Quaternion", ( ref Node node ) + { + shared quat result; + string[] vals = node.as!string.split(); + if( vals.length != 3 ) + throw new Exception( "Invalid number of values: " ~ node.as!string ); + + result.x = vals[ 0 ].to!float; + result.y = vals[ 1 ].to!float; + result.z = vals[ 2 ].to!float; + result.w = vals[ 3 ].to!float; + return result; + } ); + constructor.addConstructorMapping( "!Light-Directional", ( ref Node node ) + { + shared vec3 color; + shared vec3 dir; + node.tryFind( "Color", color ); + node.tryFind( "Direction", dir ); + return cast(Light)new shared DirectionalLight( color, dir ); + } ); + constructor.addConstructorMapping( "!Light-Ambient", ( ref Node node ) + { + shared vec3 color; + node.tryFind( "Color", color ); + return cast(Light)new shared AmbientLight( color ); + } ); + constructor.addConstructorMapping( "!Light-Point", ( ref Node node ) + { + shared vec3 color; + float radius, falloffRate; + node.tryFind( "Color", color ); + node.tryFind( "Radius", radius ); + node.tryFind( "FalloffRate", falloffRate ); + return cast(Light)new shared PointLight( color, radius, falloffRate ); + } ); + constructor.addConstructorScalar( "!Verbosity", &constructConv!Verbosity ); + constructor.addConstructorScalar( "!Keyboard", &constructConv!Keyboard ); + constructor.addConstructorScalar( "!Shader", ( ref Node node ) => Shaders.get( node.get!string ) ); + //constructor.addConstructorScalar( "!Texture", ( ref Node node ) => Assets.get!Texture( node.get!string ) ); + //constructor.addConstructorScalar( "!Mesh", ( ref Node node ) => Assets.get!Mesh( node.get!string ) ); + //constructor.addConstructorScalar( "!Material", ( ref Node node ) => Assets.get!Material( node.get!string ) ); +} + /** * Process all yaml files in a directory. * @@ -58,7 +130,7 @@ Node[] loadYamlDocuments( string folder ) foreach( file; FilePath.scanDirectory( folder, "*.yml" ) ) { auto loader = Loader( file.fullPath ); - loader.constructor = Config.constructor; + loader.constructor = constructor; try { @@ -76,7 +148,7 @@ Node[] loadYamlDocuments( string folder ) } else { - auto fileNode = Config.get!Node( folder.fileToYaml, contentNode ); + auto fileNode = contentNode.find( folder.fileToYaml ); foreach( string fileName, Node fileContent; fileNode ) { @@ -103,7 +175,7 @@ Node[] loadYamlDocuments( string folder ) */ T[] loadYamlObjects( T )( string folder ) { - return folder.loadYamlDocuments.map!(yml => Config.toObject!T( yml ) ); + return folder.loadYamlDocuments.map!(yml => yml.toObject!T() ); } /** @@ -117,7 +189,7 @@ Node loadYamlFile( string filePath ) if( contentNode.isNull ) { auto loader = Loader( filePath ~ ".yml" ); - loader.constructor = Config.constructor; + loader.constructor = constructor; try { return loader.load(); @@ -130,8 +202,259 @@ Node loadYamlFile( string filePath ) } else { - return Config.get!Node( filePath.fileToYaml, contentNode ); + return contentNode.find( filePath.fileToYaml ); + } +} + +/** + * Get the element, cast to the given type, at the given path, in the given node. + * + * Params: + * node = The node to search. + * path = The path to find the item at. + */ +final T find( T = Node )( Node node, string path ) +{ + T temp; + if( node.tryFind( path, temp ) ) + return temp; + else + throw new YAMLException( "Path " ~ path ~ " not found in the given node." ); +} +/// +unittest +{ + import std.stdio; + import std.exception; + + writeln( "Dash Config find unittest" ); + + auto n1 = Node( [ "test1": 10 ] ); + + assert( n1.find!int( "test1" ) == 10, "Config.find error." ); + + assertThrown!YAMLException(n1.find!int( "dontexist" )); + + // nested test + auto n2 = Node( ["test2": n1] ); + auto n3 = Node( ["test3": n2] ); + + assert( n3.find!int( "test3.test2.test1" ) == 10, "Config.find nested test failed"); + + auto n4 = Loader.fromString( + "test3:\n" + " test2:\n" + " test1: 10").load; + assert( n4.find!int( "test3.test2.test1" ) == 10, "Config.find nested test failed"); +} + +/** + * Try to get the value at path, assign to result, and return success. + * + * Params: + * node = The node to search. + * path = The path to look for in the node. + * result = [ref] The value to assign the result to. + * + * Returns: Whether or not the path was found. + */ +final bool tryFind( T )( Node node, string path, ref T result ) nothrow @safe +{ + // If anything goes wrong, it means the node wasn't found. + scope( failure ) return false; + + Node res; + bool found = node.tryFind( path, res ); + + if( found ) + { + static if( !isSomeString!T && is( T U : U[] ) ) + { + assert( res.isSequence, "Trying to access non-sequence node " ~ path ~ " as an array." ); + + foreach( Node element; res ) + result ~= element.get!U; + } + else + { + result = res.get!T; + } + } + + return found; +} + +/// ditto +final bool tryFind( T: Node )( Node node, string path, ref T result ) nothrow @safe +{ + // If anything goes wrong, it means the node wasn't found. + scope( failure ) return false; + + Node current; + string left; + string right = path; + + for( current = node; right.length; ) + { + auto split = right.countUntil( '.' ); + + if( split == -1 ) + { + left = right; + right.length = 0; + } + else + { + left = right[ 0..split ]; + right = right[ split + 1..$ ]; + } + + if( !current.isMapping || !current.containsKey( left ) ) + return false; + + current = current[ left ]; + } + + result = current; + + return true; +} + +/// ditto +final bool tryFind( T = Node )( Node node, string path, ref Variant result ) nothrow @safe +{ + // Get the value + T temp; + bool found = node.tryFind( path, temp ); + + // Assign and return results + if( found ) + result = temp; + + return found; +} + +/** + * You may not get a variant from a node. You may assign to one, + * but you must specify a type to search for. + */ +@disable bool tryFind( T: Variant )( Node node, string path, ref Variant result ); + +unittest +{ + import std.stdio; + writeln( "Dash Config tryFind unittest" ); + + auto n1 = Node( [ "test1": 10 ] ); + + int val; + assert( n1.tryFind( "test1", val ), "Config.tryFind failed." ); + assert( !n1.tryFind( "dontexist", val ), "Config.tryFind returned true." ); +} + +/** + * Get element as a file path relative to the content home. + * + * Params: + * node = The node to search for the path in. + * path = The path to search the node for. + * + * Returns: The value at path relative to FilePath.ResourceHome. + */ +final string findPath( Node node, string path ) +{ + return FilePath.ResourceHome ~ node.find!string( path );//buildNormalizedPath( FilePath.ResourceHome, get!string( path ) );; +} + +/** + * Get a YAML map as a D object of type T. + * + * Params: + * T = The type to get from the node. + * node = The node to turn into the object. + * + * Returns: An object of type T that has all fields from the YAML node assigned to it. + */ +final T getObject( T )( Node node ) +{ + T toReturn; + + static if( is( T == class ) ) + toReturn = new T; + + // Get each member of the type + foreach( memberName; __traits(derivedMembers, T) ) + { + // Make sure member is accessable + enum protection = __traits( getProtection, __traits( getMember, toReturn, memberName ) ); + static if( protection == "public" || protection == "export" && + __traits( compiles, isMutable!( __traits( getMember, toReturn, memberName ) ) ) ) + { + // If it is a field and not a function, tryGet it's value + static if( !__traits( compiles, ParameterTypeTuple!( __traits( getMember, toReturn, memberName ) ) ) && + !__traits( compiles, isBasicType!( __traits( getMember, toReturn, memberName ) ) ) ) + { + // Make sure member is mutable + static if( isMutable!( typeof( __traits( getMember, toReturn, memberName ) ) ) ) + { + node.tryFind( memberName, __traits( getMember, toReturn, memberName ) ); + } + } + else + { + // Iterate over each overload of the function (common to have getter and setter) + foreach( func; __traits( getOverloads, T, memberName ) ) + { + enum funcProtection = __traits( getProtection, func ); + static if( funcProtection == "public" || funcProtection == "export" ) + { + // Get the param types of the function + alias params = ParameterTypeTuple!func; + + // If it can be a setter and is a property + static if( params.length == 1 && ( functionAttributes!func & FunctionAttribute.property ) ) + { + // Else, set as temp + static if( is( params[ 0 ] == enum ) ) + { + string tempValue; + } + else + { + params[ 0 ] otherTempValue; + auto tempValue = cast()otherTempValue; + } + + if( node.tryFind( memberName, tempValue ) ) + mixin( "toReturn." ~ memberName ~ " = tempValue.to!(params[0]);" ); + } + } + } + } + } } + + return toReturn; +} +unittest +{ + import std.stdio; + writeln( "Dash Config getObject unittest" ); + + auto t = Node( ["x": 5, "y": 7, "z": 9] ).getObject!Test(); + + assert( t.x == 5 ); + assert( t.y == 7 ); + assert( t.z == 9 ); +} +version(unittest) class Test +{ + int x; + int y; + private int _z; + + @property int z() { return _z; } + @property void z( int newZ ) { _z = newZ; } } /** @@ -139,44 +462,21 @@ Node loadYamlFile( string filePath ) */ final abstract class Config { -static: -private: - Node config; - Constructor constructor; - -public: +public static: /** * TODO */ final void initialize() { - constructor = new Constructor; - contentNode = Node( YAMLNull() ); - - constructor.addConstructorScalar( "!Vector2", &constructVector2 ); - constructor.addConstructorMapping( "!Vector2-Map", &constructVector2 ); - constructor.addConstructorScalar( "!Vector3", &constructVector3 ); - constructor.addConstructorMapping( "!Vector3-Map", &constructVector3 ); - constructor.addConstructorScalar( "!Quaternion", &constructQuaternion ); - constructor.addConstructorMapping( "!Quaternion-Map", &constructQuaternion ); - constructor.addConstructorScalar( "!Verbosity", &constructConv!Verbosity ); - constructor.addConstructorScalar( "!Keyboard", &constructConv!Keyboard ); - constructor.addConstructorScalar( "!Shader", ( ref Node node ) => Shaders.get( node.get!string ) ); - constructor.addConstructorMapping( "!Light-Directional", &constructDirectionalLight ); - constructor.addConstructorMapping( "!Light-Ambient", &constructAmbientLight ); - constructor.addConstructorMapping( "!Light-Point", &constructPointLight ); - //constructor.addConstructorScalar( "!Texture", ( ref Node node ) => Assets.get!Texture( node.get!string ) ); - //constructor.addConstructorScalar( "!Mesh", ( ref Node node ) => Assets.get!Mesh( node.get!string ) ); - //constructor.addConstructorScalar( "!Material", ( ref Node node ) => Assets.get!Material( node.get!string ) ); - version( EmbedContent ) { logDebug( "Using imported Content.yml file." ); assert( contentYML, "EmbedContent version set, mixin not used." ); - import std.stream; - auto loader = Loader( new MemoryStream( cast(char[])contentYML ) ); + auto loader = Loader.fromString( contentYML ); loader.constructor = constructor; contentNode = loader.load(); + // Null content yml so it can be collected. + contentYML = null; } else { @@ -194,6 +494,8 @@ public: config = loadYamlFile( FilePath.Resources.ConfigFile ); } + deprecated( "Use global find functions instead." ) + { /** * Get the element, cast to the given type, at the given path, in the given node. */ @@ -322,18 +624,6 @@ public: @disable bool tryGet( T: Variant )( string path, ref T result, Node node = config ); - unittest - { - import std.stdio; - writeln( "Dash Config tryGet unittest" ); - - auto n1 = Node( [ "test1": 10 ] ); - - int val; - assert( Config.tryGet( "test1", val, n1 ), "Config.tryGet failed." ); - assert( !Config.tryGet( "dontexist", val, n1 ), "Config.tryGet returned true." ); - } - /** * Get element as a file path. */ @@ -423,136 +713,7 @@ public: @property int z() { return _z; } @property void z( int newZ ) { _z = newZ; } } -} - -/** - * TODO - */ -shared(vec2) constructVector2( ref Node node ) -{ - shared vec2 result; - - if( node.isMapping ) - { - result.x = node[ "x" ].as!float; - result.y = node[ "y" ].as!float; } - else if( node.isScalar ) - { - string[] vals = node.as!string.split(); - - if( vals.length != 2 ) - { - throw new Exception( "Invalid number of values: " ~ node.as!string ); - } - - result.x = vals[ 0 ].to!float; - result.y = vals[ 1 ].to!float; - } - - return result; -} - -/** - * TODO - */ -shared(vec3) constructVector3( ref Node node ) -{ - shared vec3 result; - - if( node.isMapping ) - { - result.x = node[ "x" ].as!float; - result.y = node[ "y" ].as!float; - result.z = node[ "z" ].as!float; - } - else if( node.isScalar ) - { - string[] vals = node.as!string.split(); - - if( vals.length != 3 ) - { - throw new Exception( "Invalid number of values: " ~ node.as!string ); - } - - result.x = vals[ 0 ].to!float; - result.y = vals[ 1 ].to!float; - result.z = vals[ 2 ].to!float; - } - - return result; -} - -/** - * TODO - */ -shared(quat) constructQuaternion( ref Node node ) -{ - shared quat result; - - if( node.isMapping ) - { - result.x = node[ "x" ].as!float; - result.y = node[ "y" ].as!float; - result.z = node[ "z" ].as!float; - result.w = node[ "w" ].as!float; - } - else if( node.isScalar ) - { - string[] vals = node.as!string.split(); - - if( vals.length != 3 ) - { - throw new Exception( "Invalid number of values: " ~ node.as!string ); - } - - result.x = vals[ 0 ].to!float; - result.y = vals[ 1 ].to!float; - result.z = vals[ 2 ].to!float; - result.w = vals[ 3 ].to!float; - } - - return result; -} - -/** - * TODO - */ -Light constructAmbientLight( ref Node node ) -{ - shared vec3 color; - Config.tryGet( "Color", color, node ); - - return cast()new shared AmbientLight( color ); -} - -/** - * TODO - */ -Light constructDirectionalLight( ref Node node ) -{ - shared vec3 color; - shared vec3 dir; - - Config.tryGet( "Color", color, node ); - Config.tryGet( "Direction", dir, node ); - - return cast()new shared DirectionalLight( color, dir ); -} - -/** - * TODO - */ -Light constructPointLight( ref Node node ) -{ - shared vec3 color; - float radius, falloffRate; - - Config.tryGet( "Color", color, node ); - Config.tryGet( "Radius", radius, node ); - Config.tryGet( "FalloffRate", falloffRate, node ); - - return cast()new shared PointLight( color, radius, falloffRate ); } /** diff --git a/source/utility/filepath.d b/source/utility/filepath.d index 06733f51..fc727645 100644 --- a/source/utility/filepath.d +++ b/source/utility/filepath.d @@ -5,7 +5,7 @@ module utility.filepath; import utility.output; static import std.file, std.path; -import std.stdio; +import std.stdio, std.array; /** * A class which stores default resource paths, and handles path manipulation. @@ -20,7 +20,7 @@ private: string _directory; string _extension; File* file; - + public: /** * The path to the resources home folder. @@ -42,7 +42,7 @@ public: UI = ResourceHome ~ "/UI", ConfigDir = ResourceHome ~ "/Config", ConfigFile = ConfigDir ~ "/Config", - InputBindings = ConfigDir ~ "/Input", + InputBindings = ConfigDir ~ "/Input", CompactContentFile = ResourceHome ~ "/Content", } @@ -61,27 +61,16 @@ public: } // Start array - auto files = new FilePath[ 1 ]; - uint filesFound = 0; - - // Add file to array - void handleFile( string name ) - { - if( filesFound == files.length ) - files.length *= 2; + FilePath[] files; - files[ filesFound++ ] = new FilePath( name ); - } + auto dirs = pattern.length + ? std.file.dirEntries( safePath, pattern, std.file.SpanMode.breadth ).array + : std.file.dirEntries( safePath, std.file.SpanMode.breadth ).array; // Find files - if( pattern.length ) - foreach( name; std.file.dirEntries( safePath, pattern, std.file.SpanMode.breadth ) ) - handleFile( name ); - else - foreach( name; std.file.dirEntries( safePath, std.file.SpanMode.breadth ) ) - handleFile( name ); - - files.length = filesFound; + foreach( entry; dirs ) + if( entry.isFile ) + files ~= new FilePath( entry.name ); return files; } @@ -138,7 +127,9 @@ public: } /** - * TODO + * Read the contents of the file. + * + * Returns: The contents of a file as a string. */ final string getContents() { @@ -147,6 +138,9 @@ public: /** * Create an instance based on a given file path. + * + * Params: + * path = The path of the file created. */ this( string path ) { diff --git a/source/utility/input.d b/source/utility/input.d index 4bf187ee..e1414b3f 100644 --- a/source/utility/input.d +++ b/source/utility/input.d @@ -16,6 +16,9 @@ shared static this() Input = new shared InputManager; } +/** + * Manages all input events. + */ shared final class InputManager { public: @@ -111,7 +114,7 @@ public: /** * Add an event to be fired when the given key changes. - * + * * Params: * keyCode = The code of the key to add the event to. * func = The function to call when the key state changes. @@ -145,7 +148,7 @@ public: /** * Add an event to be fired when the given key changes. - * + * * Params: * inputName = The name of the input to add the event to. * func = The function to call when the key state changes. @@ -195,7 +198,7 @@ public: /** * Check if a given key is down. - * + * * Params: * keyCode = The code of the key to check. * checkPrevious = Whether or not to make sure the key was down last frame. @@ -224,7 +227,7 @@ public: /** * Check if a given key is up. - * + * * Params: * keyCode = The code of the key to check. * checkPrevious = Whether or not to make sure the key was up last frame. diff --git a/source/utility/output.d b/source/utility/output.d index 57c7ac65..e4226985 100644 --- a/source/utility/output.d +++ b/source/utility/output.d @@ -8,23 +8,23 @@ import std.conv; import std.stdio; import std.functional; -// to not import dlogg every time you call log function -public import dlogg.log : LoggingLevel; - /** - * The types of output. - * Deprecated: use $(B LoggingLevel). - */ +* Custom logging level type for global logger. +*/ enum OutputType { - /// Info for developers. + /// Debug messages, aren't compiled in release version Debug, - /// Purely informational. + /// Diagnostic messages about program state Info, - /// Something went wrong, but it's recoverable. + /// Non fatal errors Warning, - /// The ship is sinking. + /// Fatal errors that usually stop application Error, + /// Messages of the level don't go to output. + /// That used with minLoggingLevel and minOutputLevel + /// to suppress any message. + Muted } /** @@ -33,8 +33,8 @@ enum OutputType enum Verbosity { /// Show me everything++. - /// Deprecated, debug msgs are cut off in release - /// version anyway. So equal High. + /// Debug msgs are cut off in release + /// version. Debug, /// Show me everything. High, @@ -53,20 +53,18 @@ enum Verbosity * messages - compile-time tuple of printable things * to be written into the log. */ -void log( A... )( LoggingLevel type, lazy A messages ) +void log( A... )( OutputType type, lazy A messages ) { Logger.log( messages.text, type ); } -/// Wrapper for logging with Notice level -alias logNotice = curry!( log, LoggingLevel.Notice ); -/// Alias for backward compatibility -alias logInfo = logNotice; +/// Wrapper for logging with Info level +alias logInfo = curry!( log, OutputType.Info ); +alias logNotice = logInfo; /// Wrapper for logging with Warning level -alias logWarning = curry!( log, LoggingLevel.Warning ); -/// Wrapper for logging with Fatal level -alias logFatal = curry!( log, LoggingLevel.Fatal ); -/// Alias for backward compatibility -alias logError = logFatal; +alias logWarning = curry!( log, OutputType.Warning ); +/// Wrapper for logging with Error level +alias logError = curry!( log, OutputType.Error ); +alias logFatal = logError; /// Special case is debug logging /** @@ -74,14 +72,7 @@ alias logError = logFatal; */ void logDebug( A... )( A messages ) { - Logger.logDebug( messages ); -} - -/// Alias for Output.printMessage -deprecated("Use dlogg.log.LoggingLevel instead") -void log( A... )( OutputType type, A messages ) -{ - Output.log( type, messages ); + debug Logger.log( messages.text, OutputType.Debug ); } /** @@ -100,20 +91,24 @@ void bench( alias func )( lazy string name ) /// Global instance of logger shared GlobalLogger Logger; -// Deprecated manager for console output -shared OutputManager Output; - shared static this() { Logger = new shared GlobalLogger; - Output = new shared OutputManager; } /** -* Children of StrictLogger with $(B initialize) method to +* Children of StyledStrictLogger with $(B initialize) method to * handle loading verbosity from config. +* +* Overwrites default style to use with local OutputType. */ -shared final class GlobalLogger : StrictLogger +shared final class GlobalLogger : StyledStrictLogger!(OutputType + , OutputType.Debug, "Debug: %1$s", "[%2$s] Debug: %1$s" + , OutputType.Info, "Info: %1$s", "[%2$s] Info: %1$s" + , OutputType.Warning, "Warning: %1$s", "[%2$s] Warning: %1$s" + , OutputType.Error, "Error: %1$s", "[%2$s] Error: %1$s" + , OutputType.Muted, "", "" + ) { enum DEFAULT_LOG_NAME = "dash-preinit.log"; @@ -127,20 +122,6 @@ shared final class GlobalLogger : StrictLogger */ final void initialize() { - // Verbosity is more clearer for users than logging level - LoggingLevel mapVerbosity(Verbosity verbosity) - { - final switch(verbosity) - { - // Debug messages are cut off in release version any way - case(Verbosity.Debug): return LoggingLevel.Notice; - case(Verbosity.High): return LoggingLevel.Notice; - case(Verbosity.Medium): return LoggingLevel.Warning; - case(Verbosity.Low): return LoggingLevel.Fatal; - case(Verbosity.Off): return LoggingLevel.Muted; - } - } - debug enum section = "Debug"; else enum section = "Release"; @@ -150,7 +131,7 @@ shared final class GlobalLogger : StrictLogger // Try to get new path for logging string newFileName; - if( Config.tryGet!string( LognameSection, newFileName ) ) + if( config.tryFind( LognameSection, newFileName ) ) { string oldFileName = this.name; try @@ -171,99 +152,26 @@ shared final class GlobalLogger : StrictLogger // Try to get output verbosity from config Verbosity outputVerbosity; - if( Config.tryGet!Verbosity( OutputVerbositySection, outputVerbosity ) ) + if( config.tryFind( OutputVerbositySection, outputVerbosity ) ) { - minOutputLevel = mapVerbosity( outputVerbosity ); + minOutputLevel = cast(OutputType)( outputVerbosity ); } else { - debug minOutputLevel = LoggingLevel.Notice; - else minOutputLevel = LoggingLevel.Warning; + debug minOutputLevel = OutputType.Info; + else minOutputLevel = OutputType.Warning; } // Try to get logging verbosity from config Verbosity loggingVerbosity; - if( Config.tryGet!Verbosity( LoggingVerbositySection, loggingVerbosity ) ) + if( config.tryFind( LoggingVerbositySection, loggingVerbosity ) ) { - minLoggingLevel = mapVerbosity( loggingVerbosity ); + minLoggingLevel = cast(OutputType)( loggingVerbosity ); } else { - debug minLoggingLevel = LoggingLevel.Notice; - else minLoggingLevel = LoggingLevel.Warning; - } - } -} - -/** - * Static class for handling interactions with the console. - * Deprecated: use GlobalLogger instead - */ -shared final class OutputManager -{ -public: - /** - * Initialize the controller. - */ - final void initialize() - { - verbosity = Verbosity.High; - Config.tryGet!Verbosity( "Game.Verbosity", verbosity ); - } - - /** - * Print a message to the console. - */ - synchronized final void log( A... )( OutputType type, A messages ) if( A.length > 0 ) - { - if( shouldPrint( type ) ) - { - write( getHeader( type ) ); - - foreach( msg; messages ) - write( msg ); - - writeln(); + debug minLoggingLevel = OutputType.Info; + else minLoggingLevel = OutputType.Warning; } } - -private: - /** - * Caches the verbosity set in the config. - */ - Verbosity verbosity; - - this() - { - verbosity = Verbosity.High; - } - - /** - * Gets the header for the given output type. - */ - final string getHeader( OutputType type ) - { - final switch( type ) with( OutputType ) - { - case Info: - //SetConsoleTextAttribute( hConsole, 15 ); - return "[INFO] "; - case Warning: - //SetConsoleTextAttribute( hConsole, 14 ); - return "[WARNING] "; - case Error: - //SetConsoleTextAttribute( hConsole, 12 ); - return "[ERROR] "; - case Debug: - return "[DEBUG] "; - } - } - - /** - * TODO - */ - final bool shouldPrint( OutputType type ) - { - return type >= verbosity; - } } diff --git a/source/utility/package.d b/source/utility/package.d index ec1becb1..3ae94e4b 100644 --- a/source/utility/package.d +++ b/source/utility/package.d @@ -8,3 +8,4 @@ import utility.time; import utility.string; import utility.concurrency; import utility.tasks; +import utility.array; diff --git a/source/utility/string.d b/source/utility/string.d index 14c58794..400c5a29 100644 --- a/source/utility/string.d +++ b/source/utility/string.d @@ -3,16 +3,18 @@ */ module utility.string; -import std.array; +import std.array, std.traits; -/// fromStringz /** -* Returns new string formed from C-style (null-terminated) string $(D msg). Usefull -* when interfacing with C libraries. For D-style to C-style convertion use std.string.toStringz. -* -* Authors: NCrashed -*/ -string fromStringz(const char* msg) nothrow + * Returns new string formed from C-style (null-terminated) string $(D msg). Usefull + * when interfacing with C libraries. For D-style to C-style convertion use std.string.toStringz. + * + * Params: + * msg = The C string to convert. + * + * Authors: NCrashed + */ +string fromStringz( const char* msg ) pure nothrow { scope(failure) return ""; if( msg is null ) return ""; @@ -23,7 +25,7 @@ string fromStringz(const char* msg) nothrow { buff.put(msg[i++]); } - + return buff.data.idup; } /// Example @@ -34,3 +36,33 @@ unittest assert(cstring.ptr.fromStringz == "some string"); assert(null.fromStringz == ""); } + +/** + * Replaces each key in replaceMap with it's value. + * + * Params: + * base = The string to replace on. + * replaceMap = The map to use to replace things. + * + * Returns: The updated string. + */ +T replaceMap( T, TKey, TValue )( T base, TKey[TValue] replaceMap ) pure @safe nothrow + if( isSomeString!T && isSomeString!TKey && isSomeString!TValue ) +{ + scope(failure) return ""; + if( base is null ) return ""; + + auto result = base; + + foreach( key, value; replaceMap ) + { + result = result.replace( key, value ); + } + + return result; +} +/// Example +unittest +{ + assert( "$val1 $val2 val3".replaceMap( [ "$val1": "test1", "$val2": "test2", "$val3": "test3" ] ) == "test1 test2 val3" ); +} diff --git a/source/utility/tasks.d b/source/utility/tasks.d index 522f5481..19c23dc8 100644 --- a/source/utility/tasks.d +++ b/source/utility/tasks.d @@ -80,7 +80,8 @@ unittest * scheduleInterpolateTask( transform.position, startNode, endNode, 100.msecs ); * --- */ -void scheduleInterpolateTask( string prop, T, Owner )( ref Owner own, T start, T end, Duration duration, T function( T, T, float ) interpFunc = &lerp!T ) if( is_vector!T || is_quaternion!T ) +void scheduleInterpolateTask( string prop, T, Owner )( ref Owner own, T start, T end, Duration duration, T function( T, T, float ) interpFunc = &lerp!T ) + if( ( is_vector!T || is_quaternion!T ) && __traits( compiles, mixin( "own." ~ prop ) ) ) { auto startTime = Time.totalTime; scheduleTimedTask( duration, ( elapsed ) @@ -253,6 +254,86 @@ void scheduleDelayedTask( void delegate() dg, Duration delay ) } ); } +/** + * Schedule a task to be executed on an interval, until the task returns true. + * + * Params: + * interval = The interval on which to call this task. + * dg = The task to execute. + */ +void scheduleIntervaledTask( Duration interval, bool delegate() dg ) +{ + auto startTime = Time.totalTime; + auto timeTilExe = interval.toSeconds; + scheduleTask( { + timeTilExe -= Time.deltaTime; + if( timeTilExe <= 0 ) + { + if( dg() ) + return true; + + timeTilExe = interval.toSeconds; + } + + return false; + } ); +} + +/** + * Schedule a task to be executed on an interval a given number of times. + * + * Params: + * interval = The interval on which to call this task. + * numExecutions = The number of time to execute the task. + * dg = The task to execute. + */ +void scheduleIntervaledTask( Duration interval, uint numExecutions, void delegate() dg ) +{ + auto startTime = Time.totalTime; + auto timeTilExe = interval.toSeconds; + uint executedTimes = 0; + scheduleTask( { + timeTilExe -= Time.deltaTime; + if( timeTilExe <= 0 ) + { + dg(); + + ++executedTimes; + timeTilExe = interval.toSeconds; + } + + return executedTimes == numExecutions; + } ); +} + +/** + * Schedule a task to be executed on an interval a given number of times, or until the event returns true. + * + * Params: + * interval = The interval on which to call this task. + * numExecutions = The number of time to execute the task. + * dg = The task to execute. + */ +void scheduleIntervaledTask( Duration interval, uint numExecutions, bool delegate() dg ) +{ + auto startTime = Time.totalTime; + auto timeTilExe = interval.toSeconds; + uint executedTimes = 0; + scheduleTask( { + timeTilExe -= Time.deltaTime; + if( timeTilExe <= 0 ) + { + if( dg() ) + return true; + + ++executedTimes; + timeTilExe = interval.toSeconds; + } + + return executedTimes == numExecutions; + } ); +} + /** * Executes all scheduled tasks. */ diff --git a/source/utility/time.d b/source/utility/time.d index 0d77d86f..4cb9c0b6 100644 --- a/source/utility/time.d +++ b/source/utility/time.d @@ -56,7 +56,10 @@ public: } private: - this() { } + this() + { + delta = total = Duration.zero; + } } private: @@ -76,8 +79,6 @@ static this() cur = prev = TickDuration.min; total = delta = second = Duration.zero; frameCount = 0; - - Time.delta = Time.total = Duration.min; } /**