UI
The game has a declarative UI system where UI can be defined in xml and its behavior scripted with lua. UI programming attempts to follow an MVVM pattern and is loosely inspired by WPF. Layers of UI have viewmodel lua table objects. These contain properties which the UI widgets (the "view") bind to either one way or two way bindings. The overall goal is to never write any "viewmodel" type code in C, which the language really isn't suited to.
UI Implementation
The game engine defines the XMLUIGameLayer game framework layer type. To initialize one of these you pass it an xml file, below is a minimal examle
<UIroot>
<atlas binary="./Assets/ui_atlas.atlas">
</atlas>
<screen viewmodelFile="./Assets/settings.lua" viewmodelFunction="GetSettingsViewModel">
</screen>
</UIroot>
The two important top level elements are atlas and screen. Atlas can point to a pre-compiled atlas or it can describe multiple image files and the atlas will be compiled at runtime (not recommended):
<UIroot>
<atlas>
<sprite source="./Assets/Image/kenney_ui-pack/PNG/Grey/Default/slide_horizontal_color.png" top="0" left="0" width="96" height="16" name="defaultRailHorizontal"/>
<sprite source="./Assets/Image/kenney_ui-pack/PNG/Grey/Default/slide_horizontal_color_section.png" top="0" left="0" width="16" height="16" name="defaultSliderHorizontal"/>
<font source="./Assets/Starzy_Darzy_lowercase_letters.ttf" name="default" options="normal">
<size type="pts" val="16"/>
<size type="pts" val="32"/>
</font>
</atlas>
<screen viewmodelFile="./Assets/settings.lua" viewmodelFunction="GetSettingsViewModel">
</screen>
</UIroot>
The screen element must point to a lua script file and a function that returns the viewmodel. A minimal lua file for the above would look like this:
-- file "./Assets/settings.lua
function GetSettingsViewModel()
return {
_
-- the view model implementation:
-- - fields
-- - binding functions
-- - event handlers
--- - widget mouse events
--- - OnInit
--- - button presses
--- - GameFrameworkEvent handlers
}
end
Add widgets as children of the screen element:
- The screen should behave the same same as a canvas type element, with a fixed size that is:
- immediate children can set a
dockPoint attribute to be positioned at certain points on the screen, valid values are:
centre
middleLeft
bottomLeft
bottomMiddle
bottomRight
middleRight
topRight
topMiddle
topLeft
- they're translated from their position by an offset
x and y attributes, (specified in pixels)
- they're surrounded by a given amount of padding (specified in pixels), set with the following attributes (specified in pixels):
paddingTop
paddingBottom
paddingLeft
paddingRight
- canvas element can have its size set in various ways,
- fixed size ie
height="128px" - same as the screen element
- stretch ie
height="*" - fill all available space in parent
- stretch fractoin ie
height="2*" - all the parents children have this type of dimension, and each one is a fraction of the total "*" - so if there were two children A and B with width(A) = 1* and width(B) = 2*, B would have 2/3rds the with of the parent and A would have 1/3rd
- there is also "auto", where the width and height take up the minimum size that contains their children, but I'm pretty sure this doesn't work properly
- The screen and canvas type widget mentioned above are widgets that arrange their children, in addition to this there is the stack panel widget:
- Lays its children out next to one another in either a horizontal or vertical stack
- if the children are of different sizes, the smaller ones will be aligned according to the widgets
horizontalAlignment and verticalAlignment attributes.
horizontalAlignment can have the values:
verticalAlignment can have the values:
- padding is applied
- width and height of the element is the width and height of the laid out children ie
auto
In terms of widgets that actually implement an UI element there are (the names below are the exact names the xml nodes must have):
backgroundbox
- draws a background box around its child, scales with 9 panel scaling to have bordered windows
radioGroup / radioButton
- use these two widgets to implement a radio button group
slider
static
- an image
- (name from MFC class CStatic)
textButton
- a clickable button with text in it
textInput
text
For an exhaustive list of attributes of each widget type, read the source code.
Widgets are composable, that means widgets data objects and functions can be reused to implement more complex widgets. For example the Text Button widget reuses code and structs from the background box and Text widgets.
Viewmodel Bindings
You can create a binding to an attribute in the xml (and hence a property on the widget) using the following syntax:
<slider paddingBottom="20" val="{ZoomVal}" minVal="1.0" maxVal="2.0"/>
In the above example the val property is bound to the ZoomVal viewmodel property.
The viewmodel must have at the minimum a property called ZoomVal_Get and can also have one called ZoomVal_Set to create a two way binding:
function GetSettingsViewModel()
return {
_zoomVal = 0,
-- two way binding
Get_ZoomVal = function(self)
return self._zoomVal
end,
Set_ZoomVal = function(self, val)
self._zoomVal = val
SetGameLayerZoom(self._zoomVal, self._pGamelayer)
px, py = WfGetPlayerLocation(self._pGamelayer)
CenterCameraAt(px, py, self._pGamelayer)
WfSavePreferences(self._pGamelayer) -- save to the persistant game data object
end
}
end
You can also bind to the content of a node like so, this works the same name but the binding gets or sets the text content of the node, used for the text and textInput widgets:
<textInput font="default"
colour="0,0,0,255"
paddingLeft="20"
width="200px"
fontSize="32pts"
paddingBottom="20"
maxStringLength="128"
onEnter="onTextLineEnter"
horizontalAlignment="left">
{bindingProp}
</textInput>
The stackpanel widget can bind its children to a viewmodel property:
<stackpanel childrenBinding="InventoryChildren"/>
This is really a different kind of binding to the ones above hence it doesn't use the {} syntax. The viewmodel can dynamically set the children of a widget through this binding. This doesn't exactly adhere to the MVVM pattern, but whatever.
the lua side looks like this:
-- in viewmodel table...
InventoryChildren = function(self)
self.widgetChildren = {}
for index, item in pairs(self._items) do
local spriteName = "no-item"
if item.item >= 0 then
spriteName = WfGetItemSpriteName(item.item)
end
local backgroundBoxSpriteName = "fantasy_9Panel"
if self._selectedItemIndex == index then
backgroundBoxSpriteName = "fantasy_9Panel_selected"
end
table.insert(self.widgetChildren,
{
type = "backgroundbox",
sprite = backgroundBoxSpriteName,
scaleX="1.2,",
scaleY="1.2",
paddingBottom="32",
paddingLeft="5",
paddingRight="5",
children = {
{
type = "static",
content = content,
paddingLeft = 5.0,
paddingTop = 10.0,
paddingRight = 5.0,
paddingBottom = 10.0,
scaleX="1.2",
scaleY="1.2",
sprite = spriteName,
children = {}
}
}
}
)
end
- draws a background box around its child, scales with 9 panel scaling to have bordered windows
-
radioGroup/radioButton
- use these two widgets to implement a radio button group -slider
slide it with the mouse -static
an image
(name from MFC class CStatic) -textButton
a clickable button with text in it -textInput
a text entry field -text```
- some text
For an exhaustive list of attributes of each widget type, read the source code.
Widgets are composable, that means widgets data objects and functions can be reused to implement more complex widgets. For example the Text Button widget reuses code and structs from the background box and Text widgets. return self.widgetChildren end,
In addition to the xml syntax
for creating trees of widgets, they can be created from lua tables. The
name of the element becomes a field called
"type" in the table and a field called
"children" lists the widgets children. Text content is set to the lua table field
"content".
DataNode.c/.h provide a wrapper that can either wrap a parsed xml tree or a lua table like
this and widgets construct themselves from a
DataNode.
When a bound property of any type has changed on the viewmodel side and it needs to inform the view, the lua function
const char *const name
Definition cJSON.h:270
lua OnPropertyChanged(self, "PropertyName")``` needs to be called.
- Background Box
- Radio Button / Radio Group
- Bindable fields:
- (radio group)
selectedChild
- Slider
- Static
- Text Button
- Text Entry
- Text Widget
In addition to bindings, UI events can be handled. I've kept this conceptually separate from bindings for now. All widget types can handle these mouse events:
- onMouseDown
- onMouseUp
- onMouseLeave
- onMouseEnter
they're used like so:
<backgroundboxo onMouseDown="OnNewGameMouseDown" onMouseLeave="OnNewGameMouseLeave" onMouseUp="OnNewGameMouseUp">
...
</backgroundbox>
These attributes all refer to to methods on the viewmodel object:
-- viewmodel...
OnNewGameMouseDown = function(self, x, y, button)
if not self.newButtonPressed then
self.newButtonPressed = true
OnPropertyChanged(self, "NewButtonBackgroundSprite")
end
end,
OnNewGameMouseLeave = function (self, x, y) -- OnMouseEnter is the same args
if self.newButtonPressed then
self.newButtonPressed = false
OnPropertyChanged(self, "NewButtonBackgroundSprite")
end
end,
OnNewGameMouseUp = function(self, x, y, button)
if self.newButtonPressed then
self.newButtonPressed = false
OnPropertyChanged(self, "NewButtonBackgroundSprite")
end
end,
-- viewmodel...
Some widget types also have specific events they can handle:
- TextButton
onPress - callback when pressed (for) convenience
(there are probably others)
Lua exposed functions
see engine/src/scripting/Scripting.c:
void Sc_RegisterCFunction(const char *name, int(*fn)(lua_State *))
Definition Scripting.c:360
Text rendering
Freetype is used for text rendering, font sizes are rendered ahead of time at the time of atlas creation. Rendered fonts form part of the atlas, the same as sprites.
This is only half done... better documentation needed
Testing
There is a unit test project for the engine, in the Stardew/enginetest folder. This is to mainly test code in the engine/core folder although of course can test any engine code.
It's a C++ project, so to test new areas of the C code you'll have to add this to the engine headers of any file tested:
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
In addition to the unit test project there's another integration level test project called enginenettest. This adds a new C executable that links to the engine to test the low level networking functionality (Network.c). This doesn't use any particular testing framework and is a custom made test. The test that uses this executable is the shell script EngineNetTest.sh.
One day I'll re-write EngineNetTest.sh in python so it can run in the CI on windows not just linux
Entities
The Game2D layer has an entity system whereby entity types are defined by a numerical ID, a set of callbacks, an object layer index, a transform, a user data pointer (or handle) and a list of components.
The entity callbacks are:
- Init
- Draw
- Input
- Update
- PostPhysics
- Draw
- OnDestroy
- GetBoundingBox
- GetSortPos (defines order in which entities within an object layer are drawn)
There are a fixed number of different types of components, as of writing these are:
This will be subject to change. The game won't be able to define new components so the engine should provide a good set that cover everything.
Entities can have more than one of a given component. In the case of sprites they're drawn in the order that they appear in the entity list.
Some kind of lua scripting system might be added.
Your game will define a list of entity serializers which can serialize a particular type of entity:
{
}
{
}
{
switch (version)
{
case 1:
break;
default:
break;
}
}
{
}
#define EASSERT(e)
Definition AssertLib.h:6
void BS_DeSerializeU32(u32 *val, struct BinarySerializer *pSerializer)
Definition BinarySerializer.c:234
void BS_SerializeU32(u32 val, struct BinarySerializer *pSerializer)
Definition BinarySerializer.c:119
void Et2D_RegisterEntityType(u32 typeID, struct EntitySerializerPair *pair)
Games using the engine should call this to add types of entity to be serialized.
Definition Entities.c:368
@ EBET_Last
Definition Entities.h:139
uint32_t u32
Definition IntTypes.h:12
WfEntityTypes
Definition WfEntities.h:6
@ WfEntityType_PlayerStart
Definition WfEntities.h:7
int main(int argc, char **argv)
Definition main.c:125
Definition BinarySerializer.h:16
Definition Entities.h:190
EntitySerializeFn serialize
Definition Entities.h:42
Definition Game2DLayer.h:92
See AssetTools.md to see how you can create a level full of entities that the engine can load.
Asset Tools
These are scripts and tools in the scripts folder, together they provide a means of converting source assets:
- Tiled map files
- image files, .png's, .jpegs etc
Into the game engines binary format files:
- .atlas files
- .tilemap files (misnomer: also contain entities, will be renamed)
For specific command line usage pass -h to them.
ConvertTiled.py
Create level files for your game using the program Tiled. Into the object layers you can add entities that are defined however you like defined as points, lines, polygons, boxes etc. Define the entities you want this way and give them custom properties that they will need to have. Be consistant with the class names.
Next we run a python script to convert the tiled .json file into a binary file the engine will load. To add your own type of entities to the file, extend the script the engine provides, ConvertTiled.py, like so:
import sys
import os
import struct
print(os.path.abspath("../Stardew/engine/scripts"))
sys.path.insert(1, os.path.abspath("../Stardew/engine/scripts"))
from ConvertTiled import main, register_entity_serializer, get_tiled_object_custom_prop
def serialize_WoodedArea(file, obj):
file.write(struct.pack("I", 1))
file.write(struct.pack("f", get_tiled_object_custom_prop(obj, "ConiferousPercentage")["value"]))
file.write(struct.pack("f", get_tiled_object_custom_prop(obj, "DeciduousPercentage")["value"]))
file.write(struct.pack("f", get_tiled_object_custom_prop(obj, "PerMeterDensity")["value"]))
file.write(struct.pack("f", obj["width"]))
file.write(struct.pack("f", obj["height"]))
def get_type_WoodedArea(obj):
return 6
register_entity_serializer("WoodedArea", serialize_WoodedArea, get_type_WoodedArea, False)
The ConvertTiled.py script takes a list of tiled json files as input
as output it will produce:
- for each input:
- a .tilemap binary file
- this contains the tilemaps and entities for the level
- an atlas.xml file
- this contains the file paths of all tiles used within the all for all level files passed in and their coordinates within the file, as well as their width and height
- the game can load an atlas from this directly or you can precompile it (recommended), see section below
MergeAtlases.py
A tool that merges two atlases, use like so:
python3 engine/scripts/MergeAtlases.py ./Assets/out/atlas.xml ./Assets/out/named_sprites.xml > ./Assets/out/atlascombined.xml
Use it to combine a hand written atlas of sprites used for objects in the game with the atlas of tile sprites generated by ConvertTiled.py.
AtlasTool
This is a tool written in C that will use an atlas.xml file to create an atlas of sprites that are defined in it. The game can quickly load this and use the individual sprites within, as it contains both the pixel data for the atlas and the coordinates of the individual sprites within.
The input xml files can also contain paths to fonts which will be rendered into the atlas at a specified size.
The tool does a passable but not great job of minimizing the overall size of the atlas given sprites of different sizes, it could be improved.
The good thing about this is one openGL texture can be used to draw the whole game layer, and it also means that only the tiles actually used, out of a potential source image of many more, need to be in the final loaded file.
In order to eliminate bleeding of texels from adjacent sprites the sprites in the atlas have a 1 pixel border that mimics GL_CLAMP_TO_EDGE texture clamping
ExpandAnimations.py
Expands </animation> nodes in xml files. You can write an animation node like this in an atlas xml file:
<animation name="walk-base-female-down"
fps="10.0"
source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png"
startx="0"
starty="0"
incx="64"
incy="64"
width="64"
height="64"
numFrames="9"/>
And this script will expand it like so:
<animation-frames name="walk-base-female-down" fps="10.0">
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="0" width="64" height="64" name="walk-base-female-down0" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="64" width="64" height="64" name="walk-base-female-down1" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="128" width="64" height="64" name="walk-base-female-down2" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="192" width="64" height="64" name="walk-base-female-down3" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="256" width="64" height="64" name="walk-base-female-down4" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="320" width="64" height="64" name="walk-base-female-down5" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="384" width="64" height="64" name="walk-base-female-down6" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="448" width="64" height="64" name="walk-base-female-down7" />
<sprite source="./Assets/Image/lpc_base_assets/LPC Base Assets/sprites/people/female_walkcycle.png" top="128" left="512" width="64" height="64" name="walk-base-female-down8" />
</animation-frames>
Example usage:
python3 ./engine/scripts/ExpandAnimations.py -o ./expanded_test.xml ./Assets/out/named_sprites.xml
Example Compile Assets Script
I recommend you create and maintain a compile assets script as you add assets to the game. For example:
# convert jsons from the Tiled editor to binary files containing tilemaps and entities + an atlas.xml file of the tiles used
python3 game/game_convert_tiled.py ./Assets/out -m ./Assets/Farm.json ./Assets/House.json ./Assets/RoadToTown.json
# expand animation nodes
python3 ./engine/scripts/ExpandAnimations.py -o ./Assets/out/expanded_named_sprites.xml ./Assets/out/named_sprites.xml
# merge the list of named sprites into the ones used by the tilemap
python3 engine/scripts/MergeAtlases.py ./Assets/out/atlas.xml ./Assets/out/expanded_named_sprites.xml > ./Assets/out/atlascombined.xml
# compile the atlascombined.xml into a binary atlas file
./build/atlastool/AtlasTool ./Assets/out/atlascombined.xml -o ./Assets/out/main.atlas -bmp Atlas.bmp
# compile another atlas file containing sprites and fonts for the games UI
./build/atlastool/AtlasTool ./Assets/ui_atlas.xml -o ./Assets/ui_atlas.atlas -bmp UIAtlas.bmp
A make file would be even better.
Input
There is an input mapping system: TODO: document
Game Framework
The engine is based on a simple "Game Framework" concept. This provides a stack of "layers" which may be pushed and popped. A layer is basically a set of callbacks that you can implement to implement a game such as Update, Draw, Input, etc.
The idea behind the layered concept is that layers can mask the ones below and either mask draw, update, input, or all 3. This means that a pause game framework layer can be pushed on top of the game game framework layer, masking its update and input callbacks but not the draw: the game is still rendered below the pause menu but the game is paused.
Another example would be: you travel to a new area in the game, and push that layer on top of the old one, masking it. When you leave the area back to the old one, the game framework layer is just popped off
This is a pretty simple and versatile way to have:
- different UI screens
- pause menus
- different game areas concurrently loaded
- game HUDs
The engine implements two different game framework layers:
- UI
- a declarative UI layer based on xml and lua code (see UI.md)
- Game2D
- a framework for making 2D games
- an entity system
- a tilemap system
- functions to serialize and deserialize
- drawing and culling routines
- 2D physics (using box2d library)
- structure of the main loop for this type of game
- networking
The game implemented in Stardew/src extends the Game2D layer, the idea is that a radically different type of game (such as a 3d game) could be created by implementing a Game3D layer (for example), and much of the rest of the engine would still be useful including the core library, core networking, input and the UI layer (and when i implement it, audio). New rendering functions would have to be implemented of course.
Networking
Work on this feature is currently underway.
Architecture
Aims to implement a peer to peer networking architecture in which one player is the server.
I hope to implement it in a layered and game agnostic way in the engine library. The hope is i could one day implement a completely new type of game as a gameframeworklayer (and most likely set of rendering functions) and the lowest level networking library would still be useful, as I hope would everything in the src/core/ and src/input directories and the overall game framework.
The base layer of the implementation is a core engine system that establishes a game instance as a server, client, or single player through command line arguments. Servers can run as a normal game and accept client connections at any time. The clients connect to a running server. The base layer spawns a network thread (if it's not single player) that facilitates connections, the sending and recieveing of packets and an initial level of packet processing and filtering.
Packets can be sent reliably or unreliable.
- unreliable
- these must be < 1200 bytes for now
- reliable
- these can be any size
- these will get put into the rx queue of their recipient exactly once
- if they are > 1200 the game thread will send them as a series of reliable packets that get reassembled
- just like martins geode, reliable packets must be acknowledged and will be continually resent until they are
- a circular buffer of recently acknowledged packets IDs if maintained, if one arrives after it's already been acknowledged it should be ignored
The network thread presents the game with in interface through which it can transmit and recieve these reliable and unreliable packets - a set of threadsafe queues. It does the work of reassembling fragmented packets and implementing the reliable packet acknowledgement and resending.
Network.c presents the game thread with library through which to interact with the queues, there's a tx (transmit), rx (recieve) and connectionEvents queue, the latter queues events when clients connect and disconnect.
On top of this the Game2DLayer.c adds a higher level networking protocol. I am working out the details of this and will document them at a later stage
- The overall idea for the networking of this particular game:
- The server will continually send updates of the world state to all clients
- wherever it creates the appearance of a smooth game for the client, they'll be trusted on the server but that will maintain ultimate authority
- clients trusted for their position and movement, perhaps even the result of actions like farming, but the server ensures consistency of the game world
- server assigns network ids to all entities, but if a client creates an entity it can assign it its best guess of a network ID and inform the server, which may correct the ID at a later time if there's a collision
- the network ID is a number that counts up and the client has recieved its game state from the server so its quite likely its guess of the network ID will be correct
- some of this will be on the game level and some on the engine level - it will make sense
Core networking code core/Network.c, core/Network.c
This uses the library netcode to maintain a persistent UDP connection
At a low level you must push data into the transmitting queue in the form of a shared pointer
Data recieved is a malloc'd pointer.
It is up to the function that dequeues the data to free it or decrement its reference, unless its a reliable packet in which case it will only be freed once its been acknowledged. The shared pointer ensures that big packets are only freed once all fragments have been acknowledged.
Contributing
Coding Style Aesthetics
- opening curly brackets go on a new line
- types are in PascalCase
- local variables and parameters are in camelCase
- if a variable is a pointer, name it like so:
pMyVar
- if a variable is a bool, name it like so:
bMyVar
- if a variable is global, name it like so:
gMyVar
- if a variable is "local" but static name it like so:
sMyVar
- if the variable is a handle, name it like so:
hMyVar
- if it fits into more than one of these categories, favour the g and s prefixes
- use the C style #ifndef include guard not #pragma once
Coding Style
- add a doxygen comment to all newly created public functions
- add unit tests for anything that's likely to be heavily reused, or anything that just feels complicated or brittle
- don't use macros unless absolutely necessary (and they're never really necessary) (in some places I've violated this)
- include the minimum amount of headers, especially in other headers
- don't typedef structs (in some places I have done). I don't like this because it makes them harder to forward declare.
- always typedef function pointers (in some places I haven't done). The function pointer syntax should be kept hidden away, isolated to one place.
- C code has to compile with both msvc and gcc (I've not found this to be an issue really)
- constantly check CI pipelines that both builds are succeeding and tests passing
- prefer to use python for any tools and build / testing scripts
- any python scripts should use argparse to parse command line args and should provide help strings for different flags
- Try to make the github CI script simply call scripts that also work in a local environment
- the only shell script in the yml files should be super trivial, just calling other scripts
- no github environment variables used in any scripts, they should all work in a local environment
- try to not use any external python libraries
Rendering
Currently OpenGL ES is used for rendering. Originally I had used desktop openGL, and traces of this code remain, but this should be removed. In future I'd like to support a vulkan backend as well as opengl ES.
The concept for rendering the Game2D and UI layers is simple, a buffer of verts is populated each frame and is then drawn.
- each frame there are two glBufferSubData calls (for game and then UI) and two draw calls (for game and UI), and that's more or less it for openGL calls. This should be simple to port to other rendering apis.
- this is possible because a per-gamelayer texture atlas is used
- in future I'd like to expand this to be up to 16 texture atlases per game framework layer, ie make use of all texture slots and have 3d UVs
- max opengl texture size is something like 3000x3000
- the game uses indexed vertices, the UI doesn't
- ui verts have colour attributes (for text colour)
- the game only draws tiles and entities that are in the viewport
- static entities are kept in a quad tree
- dynamic ones searched linearly
- start and finish index of tiles to draw found simply from viewport topleft/bottomright
Command Line Args
Pass command line args from your game to the engine via the EngineStart function.
The engine accepts the following command line args, mostly relating to networking and logging:
--role - networking role (-r)
--server_addres - server IP and port (-s)
--client_address - client IP and port (-c)
--log_level - drop logs that are below this level (-l)
- valid options (increasing severity):
verbose (v)
info (i)
warning (w)
error (e)
--disable_log_colour - don't use ansii colour codes in logs (looks nicer when loggin to a file)
--disable_log_timestamp - don't add a timestamp to logs
--logfile - file to log to (as well as to console)
--disable_log_tid - don't include thread id in the logged messages
--disable_console_log - don't log to console (but still log to file if one chosen)
--network_sim_config - path to network simulator config file to use
If unknown args are encountered they're just ignored so your game can handle its own arguments and then pass argc and argv
Game
Engine Design Principals
The engine is designed to provide a main loop for the game and the have the game overrides callbacks. The distinction between engine and game level code is clear but also new bits of "engine" level code can just as easily be added in the game executable if desired and then potentially moved to the engine later if deemed that they can be reused. First and foremost the game engine desires to provide solid and easy to use components that virtually all games will use (such as UI and sound, networking, input). Less focus has been placed on rendering as different games may want to take drastically different approaches. The game engine also provides "game layers" (see game framework documentation) to ease the creation of specific types of games (right now Game2D, a networked 2d game with tilemaps). If you want to make a radically different type of game, you'll implement a new game layer for your specific game type. This will include:
- the entity system
- the update / physics code
- the drawing code
but will not include:
- input
- scene management
- UI
- sound (not yet implemented)
- low level networking
The foundational core engine layer and game framework should provide a useful context to the implementation of new game layers, which should be in turn extensible by games of a particular type.
If and when new game framework layer types get implemented (for a 3d game for example) my hope is that code specific to that layer would probably find its way upstream into the shared rendering code DrawContext.c and other lower level libraries.
DrawContext.c should implement low level rendering operations.
Creating a game
To create a game, create a new executable that links to the engine:
cmake_minimum_required(VERSION 3.25)
project(MyGame)
add_subdirectory(game) # defines the target Game
target_link_libraries(Game PUBLIC StardewEngine)
In the main function of the game, call EngineStart, passing in a GameInit function that pushes a game framework layer to start your game. Your game can define its own game framework layer or use a built in one.
Here's an example of starting a game using the builtin Game2DLayer:
#include <string.h>
{
printf("seed: %u\n", seed);
options.atlasFilePath = "./Assets/out/main.atlas";
options.tilemapFilePath = "./Assets/out/Farm.tilemap";
}
int main(
int argc,
char** argv)
{
}
void Et2D_Init(RegisterGameEntitiesFn registerGameEntities)
Definition Entities.c:145
static entities are stored in here for fast querying to be rendered TODO: maybe rewrite this and repl...
void InitEntity2DQuadtreeSystem()
Definition EntityQuadtree.c:116
void Game2DLayer_Get(struct GameFrameworkLayer *pLayer, struct Game2DLayerOptions *pOptions, DrawContext *pDC)
Definition Game2DLayer.c:816
void GF_PushGameFrameworkLayer(const struct GameFrameworkLayer *layer)
Definition GameFramework.c:42
@ EnableUpdateFn
Definition GameFramework.h:22
@ EnableDrawFn
Definition GameFramework.h:23
@ EnableOnPop
Definition GameFramework.h:26
@ EnableOnPush
Definition GameFramework.h:25
@ EnableInputFn
Definition GameFramework.h:24
void Ph_Init()
Definition Physics2D.c:46
unsigned int Ra_SeedFromTime()
Definition Random.c:16
void WfRegisterEntityTypes()
Definition WfEntities.c:37
void WfInit()
Definition WfInit.c:10
int EngineStart(int argc, char **argv, GameInitFn init)
Definition main.c:222
void GameInit(InputContext *pIC, DrawContext *pDC)
Definition main.c:83
Definition DrawContext.h:59
Definition Game2DLayer.h:234
Definition GameFramework.h:42
Definition InputContext.h:137
If you use the built in Game2DLayer your game can register entity types with asset tooling and the entity system.
For more information on this see Entities.md and Assets.md
Engine Core
DynamicArray.c/.h
- A dynamic array similar to a C++ vector
- Create new with
struct MyStruct* v = VECTOR_NEW(struct MyStruct)
- push with
- when capacity is reached the vector will double in size
struct MyStruct s;
void * VectorPush(void *vector, void *item)
Definition DynArray.c:44
- pop with
void * VectorPop(void *vector)
Definition DynArray.c:57
- resize with
void * VectorResize(void *vector, unsigned int size)
Definition DynArray.c:16
- get the last item with
void * VectorTop(void *vector)
Definition DynArray.c:65
- clear the vector with
- note this doesn't deallocate any memory, that only happens when the vector is destroyed
void * VectorClear(void *vector)
Definition DynArray.c:71
- note this doesn't deallocate any memory, that only happens when the vector is destroyed
void DestoryVector(void *vector)
Definition DynArray.c:84
- Vectors can be accessed as if they were normal arrays:
struct MyStruct s = v[1];
- Size of vector can be gotten with
#define VectorSize(vector)
Definition DynArray.h:30
- they can be resized at will (this will increase their capcity but not size)
- NOTE that often the functions will return a pointer which needs to be assigned to the vector variable, in case the vector has resized and the old pointer is invalid - don't forget to make the assignment
ObjectPool.c/.h
This is an array of a specific type of object that you can request an index into, the index then serves as a handle to an instance of the object the pool holds. When the object is done with and needs to be destroyed its index is freed and the pool will give out the index once more. If there is no room in the pool and a new handle is requested it will resize (doubling in size). Even though the pool has resized, the handle (unlike a ptr would be) is still valid.
The memory for the pool consists of two areas, the pool itself and a list of free indices.
- Initialize a new object pool:
void * InitObjectPool(int objectSize, int poolInitialSize)
Definition ObjectPool.c:10
- Get a handle from the pool:
void * GetObjectPoolIndex(void *pObjectPool, int *pOutIndex)
returns the object pool, possibly resized. returns index of a free space in the pool through pOutInde...
Definition ObjectPool.c:50
- access an instance in the pool by using the handle as an index:
int hMyStruct = GetStructHandle();
struct MyStruct* c = &gObjectPool[hMyStruct];
- free a pool allocated object:
void FreeObjectPoolIndex(void *pObjectPool, int indexToFree)
Definition ObjectPool.c:61
- destroy an object pool:
void * FreeObjectPool(void *pObjectPool)
Definition ObjectPool.c:80
- NOTE - just like the vector these often return a value which needs to be assigned back to the object pool
SharedPtr.h
A simple shared pointer, like the object pool and dynamic array it stores a control struct behind the pointer that is returned to the user.
Decrement ref count and it is only freed when the ref count is zero. You can also give it a "destructor" function to call
Bitfield2D.c/.h
A 2D array of booleans implemented as a bit field. Initialize to a specifc size, get and set bits at specific x and y coordinates.
Log.c/.h
Logging system, filter by severity (info, warning, error), prints time and thread id, can log to file, console or both. Used throughout engine instead of printf
StringKeyHashMap.c/.h
A generic hash map with strings for keys. Resizes when a given load factor is reached, uses djb2 hash algorithm + linear probing. Can iterate through keys efficiently.
BinarySerializer.c/.h
A "class" for loading and saving binary data. It can be created to load or save to a file, to load from an in memory array, or to save to the network. First the binary serializer struct is initialized by calling BS_CreateForLoadFromBuffer or BS_CreateForLoad or BS_CreateForSave or BS_CreateForSaveToNetwork. Next data is serialized by calling BS_SerializeU32 or BS_DeserializeU8 etc. Next BS_Finish is called. At this point if saving the data is saved to disk or enqueued to the network. Used by game2d layer code.
DataNode.c/.h
An abstraction for parsing and loading text based data, through it code can load from a lua table or xml file. Used by UI code.
Thread.c/.h
Cross platform thread abstraction using either win32 or pthread.
ThreadSaveQueue.c/.h
Fixed size circular thread safe queue, uses cross platform Thread.c/.h abstractions. Callback if queue wraps around and data item dropped.
SharedLib.c/.h
Cross platform shared library / dll handling, wraps linux/win32 functions to explicitly load function pointers from DLLs, not actually used yet.
FloatingPointLib.c/.h
Compare floats accounting for rounding errors.
FileHelpers.c/.h
Function to load a file into a buffer
TimerPool.c/.h
Intended to be used by game layers to implement timers in a uniform way.
Random.c/.h
RNG functions
RawNetMessage.c/.h
Parse and create messages that follow the lowest level of networking protocol, goes along with Network.c
Network.c/.h
Low level networking, networking thread
Geometry.c/.h
Basic geometry functions: AABB tests etc.
ImageFieRegstry.c/.h
This is a relic from the past and serves no purpose - it needs to be completely removed TODO
Atlas.c/.h
Code to create and use a texture atlas. Load a pre-made atlas or create one from xml. Once an atlas is loaded, code to upload texture to gpu, query sprites inside it by name and fetch UVs for them etc.
Other...
engine/src/input should be considered part of the engines core as well: this contains code for abstracting input so that it is not tied to a given key or input device and remapping it.
It is my hope that one day engine/src/rendering can become a part of the core engine, and in a way it kind of is.
engine/src/scripting seems like it would form part of the core, but the code in there is really specific to the XML UI code, this file needs splitting into a reusable part and a UI code specific part. The code in it is also specifically LUA scripting.