![]() |
Squirrel Engine
|
Base Principles
Squirrel Engine is a 3D game engine featuring dynamic deserialization and physics integration. It uses a scene graph/tree to organise objects in the scene. The game engine is inspired from the design of Godot (Godot Foundation, 2007), featuring a similar object and scene layout.
Documentation for each class is available here: https://corcso.github.io/SquirrelEngine (Heesh, 2024)
Figure 1: Scene tree with main.nut loaded
Squirrel Engine is based off of the core idea of having 2 types of objects. Nuts (Nodes, Objects) and Resources
|
Nuts Uniquely owned by their parent in the scene graph. Created programmatically or instantiated from a file. Used for any world objects such as: Lights, cameras, meshes. |
Resources Shared ownership across users. Loaded from a file. Used for resources such as: Meshes, materials, collision shapes. |
Platform Support
Currently Squirrel engine supports windows builds with DirectX11 (Microsoft, 2009) for rendering and Jolt (Rouwe, no date) for physics simulation. These services have been sufficiently abstracted so that another library could be used instead.
Example Game
When creating a new project the example game project should be used. This has the entire engine with the correct compilation settings as well as a small game example (which can be easily removed) to help you get started and provide code examples.
The example features a simple shooter game. WASD to move, Space to jump, Left click to shoot, G displays pool allocator capacity, K turns the player's collider into a sphere.
If the player walks under the red spotlight a second scene will load, this is to simulate an “airlock” loading mechanic used in certain games.
Nuts
Developers are able to create their own nuts by taking an existing nut as a base class. Developers can then add functionality and custom deserialization to their nut. For example: A player nut, inherited from PhysicsNut. This could contain movement functionality and a total health, taken in from deserialization.
There are 6 nut types which come with the engine. Nut, WorldNut, MeshNut, LightNut, CameraNut and PhysicsNut. Each has their own functionality.
Figure 2: Nut class inheritance diagram. (Heesh, 2024)
Nut - A nut with only a name and child / parent information. This nut has no transform in 3D space. It has functionality required across all nuts and is the final parent class for all nuts. This nut can be used for things like folders in the scene where objects need to be placed.
WorldNut - A Nut which has a 3D transformation, a location, rotation and scale in the 3D world. All children in the scene graph inherit this transform.
MeshNut - A WorldNut which has a mesh (3D model) and material applied. This is the only renderable nut type.
CameraNut - A WorldNut which is used for cameras. Has a FOV, and an isActive property. Only one camera should be active at a time.
LightNut - A WorldNut which represents a light in the scene. There are 3 light types, Directional, Point and Spot. The engine supports up to 8 lights.
PhysicsNut - A WorldNut which undergoes physics simulation. It must have a CollisionShape attached.
To create your own type of nut for use in the scene you should:
Resources
There are 4 types of resources in Squirrel Engine: meshes, materials, collision shapes and shelled nuts.
Figure 3: Resource class inheritance diagram. (Heesh, 2024)
Mesh - A 3D model object used for rendering with a MeshNut. Currently only supports 1 object model files. .obj is recommended but other file types like .fbx should be supported. Assimp is used for loading (Kulling, no date a)
Material - .mat - A material which is applied to a mesh, contains attributes like colour and smoothness.
Collision Shape - .shape - A shape used by the physics engine to calculate collisions. There are 3 available types: Box, Sphere and Capsule.
Shelled Nut - .nut - A storage resource for holding a serialized nut scene. Used to instantiate nut scenes at runtime.
Resources should be loaded using the resource manager. Use GetResourceManager() from anywhere in your game to retrieve a pointer to the resource manager. Then use Retrieve() to retrieve a resource, this will load the resource if required.
Resource references should always be stored with a shared pointer.
Currently resources are not stored in pool allocators, so the STL shared pointer is used.
Development Environment & Getting Started
Squirrel Engine currently uses Microsoft Visual Studio as its sole development environment. You should use the example project as a starting point for your game. This contains a visual studio solution with the correct setup and a small shooter game to serve as an example to do various things.
The solution contains 2 projects. SquirrelEngine and SquirrelGame. SquirrelGame is where you will perform most of your programming.
In the SquirrelGame project folder in file explorer there is also an options.json file and a Resources folder.
The options file contains all the project settings. All options must be set in json format.
|
JSON Property Name |
Usage |
|
Window Name |
Name for the window. Appears in the top bar. |
|
Window Size |
Pixel size of the rendering window. |
|
On Load Nut |
Serialized Nut .nut file to load on start. |
|
Background Color |
The background color to be set. |
|
Target FPS |
The target FPS for the game. The game is limited to this FPS. |
Figure 4: Solution Layout Figure 5: Options.json file structure
The resources folder is where you will put all your resources and serialized nuts. All resources are referenced by their file path. You are able to create other folders within and outside the resources folder if you wish. Example path: ./Resources/wood.mat
All resources with the exception of meshes are JSON files. You are able to use any text editor to edit these files as you wish.
You may need to set the debugger working directory, in SquirrelEngine properties set Debugging->Working Directory to $(SolutionDir)SquirrelGame for all configurations.
JOLT Compilation
The example project comes with precompiled JOLT (Rouwe, no date a) binaries but these may not work, if you are receiving linker errors you should compile the JOLT .lib files yourself, follow this guide.
Nut Functions To Override
The following nut functions should be overwritten. They should also call their parent class’s respective function first.
Ready() is called once on a nut when it first enters the main scene tree. If a nut leaves the scene tree and is then re-added, ready will not be called again.
Update() is called once per frame.
LateUpdate() is called once per frame, after all updates.
The static function Deserialize() should also be added to ensure your nuts can be loaded from a .nut file. It is best to copy the deserialization function from the template and add any deserialization parameters between the following comments.
// YOUR DESERIALIZATION HERE, USE TOWORKON
and
// Return ownership of the new nut
PhysicsNuts also have OnCollisionStart(PhysicsNut*) and OnCollisionEnd(PhysicsNut*) virtual functions. These should be overridden if you would like to perform certain actions based on collisions. The physics nut pointer passed in is the object the collision has started or ended with. These are called before Update().
Maths
An edited version of handmade math (bvisness, 2016) is used for the maths functionality of Squirrel Engine. It supports two, three and four dimensional vectors as well as square matrices. It also supports quaternion operations. Due to the library supporting C as well as C++ construction of maths objects happens via separate functions, for example Vec3’s are constructed with the V3() function. Mat4’s with M4() and Quat’s with Q().
Services
There are 7 services part of the engine. These are: Graphics, Physics, Input, Time, PoolAllocationService, ResourceManager and Tree. Graphics & Physics typically are only for engine interaction. Input provides input checking functionality. Time provides functions for delta time and time since start. PoolAllocationService is used to create new nuts, see Nut Ownership below. ResourceManager is used to load resources from a file, stopping them from being loaded twice and unloading them when they are no longer needed. And finally Tree is used for general engine functions like Quit(), it also handles the game loop and stores the scene root. Each can be accessed via Services::GetXXXX() or the shorthand GetXXXX().
Input
Input is handled via the input service. Use GetInput() to access it. Input has functionality for getting key and mouse button states as well as checking mouse position and getting relative mouse movement when the mouse is locked. There are Key and MouseButton Enums for referencing keys and buttons but characters can also be used for keys.
A Button/Key has 4 states, Up, Down, Pressed and Released. Pressed and Released only trigger on the frame the key is pressed or released.
Nut Ownership
All nuts are stored within a Pool Allocator (Gregory, 2018). This allows for faster creation and deletion of nuts. Due to nuts varying size, several pool allocators are used, the Pool Allocation Service handles this for you and is able to create unique pointers to pool objects.
If there isn't a pool with big enough blocks or there is but that pool is full, the Pool Allocation Service will default to using new. UniquePoolPtr supports the use of new and delete keywords, so one is still returned.
You can edit the pool count, block sizes and block counts in the PoolAllocationService.h file.
Objects in pools must use the UniquePoolPtr unique pointer rather than the STL unique pointer.
All raw pointers to nuts should be considered observers only, and should never be deleted.
When creating a new nut programmatically you should call GetPoolAllocationService()->MakeUniquePoolPtr<NUT TYPE>();
Nuts follow a strict ownership. Once they are created their ownership must be passed around with a UniquePoolPtr at all times.
When reparenting a nut owned by another nut, you can simply call the SetParent() function and pass in the new parent.
When reparenting a nut owned by you, you must get an observer pointer (via .get()) first and then call SetParent() on that observer, not your unique pointer, also passing in your UniquePoolPtr, to hand over ownership. This can be seen in figure 8.
You can additionally use AddChild() to reparent a nut but you should only call this for adding nuts you own as it will not remove the nut as a child from its parent.
.Nut files, Instantiation & Shelled Nuts
For the starting scene of your game you must provide a .nut file, everything from here on out can be created programmatically but it is recommended to use a nut file to save time, provide cleaner code and easier editing.
Nut files are JSON files which contain a serialized scene graph. This scene can contain any type of nut, developer or engine defined, as well as any depth of children.
JSON files are parsed with the nlohmann JSON library (Lohmann, 2013)
Properties available depend on the type of nut being represented but all nuts must have a name and a type.
You are also able to create custom properties which can be deserialized on your own nut types using by editing the Deserialize function.
Figure 6: Section of secondLevel.nut from the example project
On the right is a portion of the secondLevel.nut file in the example project. It contains a scene root called “Second Level” which has a static PhysicsNut “Floor” as a child and a LightNut “Spot Light” as another child.
Below is a table containing all the properties which can be set, it is best practice to define all properties even if not using them.
|
Property |
Type |
Usage |
|
type |
String |
The type name used in the SerializationTypeDictionary to create the correct type of nut |
|
children |
Array of objects |
An array containing all child nuts |
|
Nut |
|
|
|
name |
String |
Name for the nut in the scene tree, recommended to be unique |
|
WorldNut |
|
|
|
position |
Array of 3 Numbers (Vector 3) |
Local world position of this WorldNut |
|
eulerAngles |
Array of 3 Numbers (Vector 3) |
Local euler angles of this WorldNut in radians |
|
scale |
Array of 3 Numbers (Vector 3) |
Local scale of this WorldNut |
|
MeshNut |
|
|
|
mesh |
String, Resource File Path |
Mesh to use |
|
material |
String, Resource File Path |
Material to use |
|
LightNut |
|
|
|
diffuseColor |
Array of 3 Numbers (Vector 3) |
Colour of diffuse light |
|
ambientColor |
Array of 3 Numbers (Vector 3) |
Colour of ambient light |
|
intensity |
Number, decimal |
Intensity of diffuse light |
|
ambientIntensity |
Number, decimal |
Intensity of ambient light |
|
lightType |
Enum String, one of: “directional”, “point” or “spot” |
The type of light this light nut represents |
|
spotlightInnerAngle |
Angle in radians, number, decimal |
The inner cutoff angle for spotlights |
|
spotlightOuterAngle |
Angle in radians, number, decimal |
The outer cutoff angle for spotlights |
|
linearAttenuation |
Number, decimal |
Linear attenuation, dims light over linear distance. |
|
quadraticAttenuation |
Number, decimal |
Quadratic attenuation, dims light over square distance. |
|
CameraNut |
|
|
|
fov |
Number, decimal |
Field of view of the camera |
|
isActive |
Boolean |
Boolean to set if the camera is the active renderer or not |
|
PhysicsNut |
|
|
|
static |
Boolean |
If true the object is considered static and non moving. If false the object is considered dynamic and will move. |
|
elasticity |
Number, decimal |
Also known as restitution, a number between 0 and 1 which represents bounciness. |
|
density |
Number, decimal |
Density of the object, determines mass. |
|
collisionShape |
String, Resource file path |
Path to the .shape file used for this nuts collision shape. Required |
Figure 7: Table of engine nut properties
When you load a ShelledNut that only loads the JSON serialized data into memory. The scene itself needs to be instantiated, this creates all the nuts defined in the .nut file and loads all the relevant resources.
To instantiate a shelled nut, call its Instantiate() function. This returns a unique pointer to the root nut of the scene giving you ownership. You can then add it to the relevant point in the scene graph if you wish.
Instantiating can be a long process, large instantiations should be handled via the InstantiateMultithread() function. This instead returns a promise (via shared pointer), containing a completed boolean and a unique pointer to the root nut of the scene, once it has finished loading.
The developer should poll the promise until the instantiations completion and then take ownership of the scene. An example of this can be seen in the AirlockSceneLoading nut in the example game.
Figure 8: Snippet from Update() in AirlockSceneLoading
SerializationTypeDictionary
The SerializationTypeDictionary is core to creating a game with Squirrel Engine. It is a map which maps strings to Deserialization functions. The key of the map, the string, should be the type name used in .nut files. The value, the function, should be the corresponding deserialization function to call for this type.
The SerializationTypeDictionary is defined in the engine but needs to be initialized in the game. The example game comes with one already.
Figure 9: DeserializationTypeDictionary Initialization
Deserialize Function
The deserialize function should be a static function which returns a unique nut pointer and takes in a nut pointer and some json data.
If the nut pointer is null, the deserialization function should create a new nut and use that to deserialize on.
If the nut pointer is not null, the deserialization function should perform deserialization on that nut pointer instead.
The deserialization function should also call its parent class’s function before doing any deserialization of parameters.
.shape and .mat Resources
Collision shapes and materials are also represented via JSON below are tables which detail their available properties.
|
.shape |
|
|
|
type |
Enum, String, One of: “box”, “sphere” or “capsule” |
Determines the type of collider shape. |
|
boxHalfDimensions |
Array of 3 Numbers (Vector 3) |
Dimensions from the center to the edge of the box in each direction |
|
sphereRadius |
Number, decimal |
Radius of the sphere. |
|
capsuleRadius |
Number, decimal |
Radius of the capsule hemispheres |
|
capsuleHalfHeight |
Number, decimal |
Length from the center to the edge of a hemisphere for the capsule |
|
.mat |
|
|
|
diffuse |
Array of 3 Numbers (Vector 3) |
Diffuse colour, colour of the object |
|
specular |
Array of 3 Numbers (Vector 3) |
Specular colour, typically white |
|
specularity |
Number, decimal |
Specularity, how sharp the specular highlight is. Must be 1 or above, powers of two recommended. 64 or 128 is a good value. |
|
smoothness |
Number, decimal |
Smoothness, 0 is matte, 1 is smooth |
Figure 10: Table of .shape and .mat JSON attributes
Inherited Transforms & Super local rotations
WorldNuts inherit the transforms of any parent WorldNuts in the scene.
The RotateSuperLocal functions rotate the nut around a local right up or forward direction. The local right, up or forward is the nut's right, up or forward direction rotated only by the local rotation, not any parent rotations. These can be used for things like vertical camera rotation.
Important Information about PhysicsNuts
PhysicsNuts have some properties which are not intuitive, these are explained below.
Best Practice
Below is a list of best practice rules to follow:
Error Codes
There are a total of 25 error codes which the engine can throw.
|
Error Code |
Meaning |
From Class(es) |
Function |
|
1 |
DirectX Math not Supported on CPU |
GraphicsDX11 |
init |
|
2 |
Failed to register window class |
GraphicsDX11 |
init |
|
3 |
Failed to create window |
GraphicsDX11 |
init |
|
4 |
Failed to create, DX11 Device, Device Context or Swap Chain |
GraphicsDX11 |
init |
|
5 |
Failed to get back buffer from swap chain |
GraphicsDX11 |
init |
|
6 |
Failed to create render target view |
GraphicsDX11 |
init |
|
7 |
Failed to create depth/stencil buffer |
GraphicsDX11 |
init |
|
8 |
Failed to create depth/stencil view |
GraphicsDX11 |
init |
|
9 |
Failed to create depth/stencil state |
GraphicsDX11 |
init |
|
10 |
Failed to create rasterizer state |
GraphicsDX11 |
init |
|
11 |
Options File failed to open |
Entry |
main |
|
12 |
Options file has no window name |
Entry |
main |
|
13 |
Options file has no window size |
Entry |
main |
|
14 |
Options file window size formatted incorrectly |
Entry |
main |
|
15 |
Options file has no on load nut |
Entry |
main |
|
16 |
Shelled Nut being loaded but a nut inside it has no type |
ShelledNut |
Instantiate |
|
17 |
Shelled Nut being loaded but a nut inside it has no name |
Nut |
Deserialize |
|
18 |
Error Loading Mesh |
Mesh |
Load |
|
19 |
Pool Allocation Service Unknown Error |
PoolAllocationService |
MakeUniquePoolPtr |
|
20 |
Background colour not set |
Entry |
main |
|
21 |
Background colour wrong format |
Entry |
main |
|
22 |
Trying to allocate to a full pool. |
PoolAllocator |
New & Alloc |
|
23 |
Target FPS not set |
Entry |
main |
|
24 |
Pool block size too small or blocks not aligned. Make block size more than or equal to 8, and divisible by 8. |
PoolAllocator |
PoolAllocator |
|
25 |
Physics Nut does not have a collision shape when ready called |
PhysicsNut |
Ready |
Figure 11: Error Codes Table
References
bvisness (2016) Handmade Math (v2.0.0) [C++]. https://github.com/HandmadeMath/HandmadeMath
De Vries, J. (2014) Light Casters Available at: https://learnopengl.com/Advanced-Lighting/Bloom (Accessed: 5 December 2024)
Godot Foundation (2007) Godot Accessible at: https://godotengine.org/ (Accessed On: 17 December 2024)
Gregory, J. (2018) Game engine architecture. 3rd edition. Massachusetts: A K Peters/CRC Press
Heesh, D. (2024) Doxygen v1.12.0 Accessible at: https://www.doxygen.nl/download.html (Accessed on: 17 December 2024)
Kulling, K (no date a) Assimp [C++] https://github.com/assimp/assimp (Accessed On: 13 December 2024)
Kulling, K (no date b) Importing Data Access by C++ class interface [C++] https://assimp-docs.readthedocs.io/en/latest/usage/use_the_lib.html#access-by-c-class-interface (Accessed On: 13 December 2024)
Lohmann N (2013) JSON (v3.11.3) [C++] https://github.com/nlohmann/json (Accessed On: 22 November 2024)
Microsoft (2009) DirectX 11 (v11.1) [C++] https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
Microsoft (2019) Keyboard and Mouse Input Accessible at: https://learn.microsoft.com/en-us/windows/win32/inputdev/user-input (Accessed On: 8 November 2024)
Microsoft (2023) Taking Advantage of High-Definition Mouse Movement Accessible at: https://learn.microsoft.com/en-us/windows/win32/dxtecharts/taking-advantage-of-high-dpi-mouse-movement (Accessed On: 17 November 2024)
Nystrom, R. (2014) Game Programming Patterns Unknown: Genever Benning
Rouwe, J. (no date a) Jolt [C++] https://github.com/jrouwe/JoltPhysics (Accessed On: 14 December 2024)
Rouwe, J. (no date b) JoltPhysics HelloWorld.cpp https://github.com/jrouwe/JoltPhysics/blob/master/HelloWorld/HelloWorld.cpp (Accessed On: 14 December 2024)