The coding-style used for the project is derived from the K&R https://www.kernel.org/doc/html/v4.10/process/coding-style.html
Makefile macros the controls the engine behaviour are in the form TOFU_XXX. Macro that controls only the build process (for example, to specify the platform) doesn't have this prefix.
Behavioural macros are also present in the file config.h.
The rules are simple:
- each macro should begin with the prefix that defines the "namespace" (e.g.
LUAX_XXX); - engine-wise public macros must have the
TOFU_prefix, being them "constants", functions-like, or behavioural (e.g. to configure); - for library/reusable code macros the prefix is the library name (e.g.
LUAX_XXXforluax.h); - macros that are not public have an additional single underscore as prefix (e.g.
_LUAX_XXX).
Please, don't ever a double underscore as this is meant for internally defined ones!
If a behavioural macro has a selective optional/inferred value (e.g. to be active only in DEBUG mode) this idiom is adopted
#if !defined(LUAX_NO_RTTI) && defined(DEBUG)
#define _LUAX_RTTI
#endif /* LUAX_NO_RTTI */Note that this specific is interesting: we are checking a public macro (LUAX_NO_RTTI) that will eventually be used to define another one which is private (_LUAX_RTTI).
Also note that we use 4-spaces soft-tabs also for the internal macro definition.
For the identifiers is adopted the snake-case style.
The preferred form for preprocessor conditional is the "expanded one", that is
#if defined(DEBUG)
// ...
#else /* defined(DEBUG) */
// ...
#endif /* defined(DEBUG) */over
#ifdef DEBUG
// ...
#else /* DEBUG */
// ...
#endif /* DEBUG */This is due to better consistency when more than one condition is to be checked.
The only exception to this is for the include-guards (see below), which uses the compact version, or when the condition is in form of == with combined #elif clauses.
Coincidentally this is the same approach used in Lua's codebase, with the difference that we also annotate the closing statements. :)
Every sub-system should use a macro with identifier TOFU_<module-name>_DEBUG_ENABLED to control a fine log/debug level to be used. The macro should be defined in the config.h file.
In addition the macro TOFU_CORE_VERBOSE_DEBUG is used to wrap the log information for the APIs that are more frequent in use (e.g. the loading system) and when enabled can hinder the performance significantly.
The TOFU_CORE_DEFENSIVE_CHECKS macro, otherwise, enables arguments checking in some selected functions. This is something that should be enabled only in the DEBUG build as arguments should not need any check once the "contract" between caller and callee is respected (and we tend to prefer that it should be the caller to ensure arguments coherence, for example for pointers).
Types are defined with Pascal-case style. The following suffixes are used:
_sfor structures,_efor enumerations,_ufor unions,_tfor typedefs.
The const modifier should be always used for pointers, to indicate the purpose/rose of the pointer itself, especially in a function signature. This is not something new as the standard C-library adopts this style since... ever. :) See, for example, the memcpy() function signature. This has be huge benefit that protects as much as possible from actual-argument misplacement (as a compilation error would occur).
We advice, also, to use the const modifier for integral types. We aren't referring the their usage in function signature (it would be a nice benefit, albeit pedant), but when defining a local variable that is not meant to change in the scope block. For example
const size_t length = arrlenu(resources);
for (size_t i = 0; i < length; ++i) {
// We can be sure that `length` won't change...
}Of course, global variables that are used as constants need to be declared as const, as well.
- 4 spaces indentation is used (soft tabs);
- when splitting a structure/function-call over multiple lines we use an additional 4 spaces indentation;
- source files are terminated on a new line;
- trailing spaces are to be eliminated;
- ...
Single header libraries are tolerated and used without any issue. They are, however, adapted into a header/module pair for convenience and clarity.
For example, the single header library stb_image.h would be adapted by defining the header file
#ifndef STB_IMAGE_H
#define STB_IMAGE_H
#include <stb/stb_image.h>
#endif /* STB_IMAGE_H */and the source file
#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>In C one can implement a "dependency injection" of some sort by mean of function pointers.
A typical usage is for I/O, as a way to provide some custom implementation for non-standard I/O functions. In such a scenario, the API requires to pass one or more functions pointers and a (optional) user-data. The former are very ofter packed into a structure and passed as pointer, while the latter is a generic void *.
Please note that we adopt the convention of passing the user-data as a NON CONST
void *. While there are definitely chances that the pointer is actually treated as constant (and never accessed in write mode), is a more "general purpose" approach to leave it non-constant.
We stick to this pattern in our code.
Having the two separated permits to pre-define the structure once for all and reuse it (which isn't technically an issue with C99's compound literals but, nonetheless, represents a way to both optimize and keep the code cleaner).
More precisely we avoid to either
- pass each single callback as a different argument (which would confuse the code a lot),
- pass the callbacks structure by value (occupies the stack more than it's necessary),
- pack the functions pointers AND the user-data into the same function (which might appear clever, but it's really just messy).
As a general advice we should always provide (or for better saying, favour) callback-driven I/O routines. At the price of a (small) boilerplate code addition we can support seamlessly every type of source for the data. Instead of having a
XXX_from_file(),XXX_from_memory(), and so on we can have a singleXXX_from_callbacks()that supports all of them.
According to the "purpose" of the file, it will be places in a specific folder. The rules are the following:
- the
corefolder is used to store the engine internals, pretty much everything will be included here or in a subfolder of it; - the
kernalfolder is home of the Lua runtime; - the
libsfolder contains reusable piece of code.
An header file must be constructed as follows:
- copyright/license header,
- includes section,
- module local macro definitions,
- module local variables,
- functions and procedures.
Local scoped functions/procedures/variable are static and prefixed with the _ (underscore) character. They are to be defined just before their use. For example, if we have a global function foo_bar() that uses the _baz() function, the would be defined as follows:
Also locally scoped UDTs are preceded with the
_(underscore) character.
typedef struct _object_s {
int id;
} _object_t;
static const int _value = 42;
static inline int _baz(void)
{
return _value;
}
int foo_bar(void)
{
return _baz();
}In case the body of the local function is small enough, declare it as
inlineas an hint for the compiler.
Include directives are placed at the top of the file, right after the file header banner.
They are defined with the following priority/order:
- quoted module
.hfile - quoted local
.hfiles - angled-brackets project-specific library
.hfiles - angled-brackets external library
.hfiles - angled-brackets standard library
.hfiles
This is an example usage.
#include "configuration.h"
#include "internal/parser.h"
#include <core/version.h>
#include <GLFW/glfw3.h>
#include <string.h>Include guards are used in each and every .h file. They are assigned
with the following format:
#ifndef PATH_FILE_H
#define PATH_FILE_H
#define FILE_H_INCLUDED
/* Body of the header file */
#endif /* PATH_FILE_H */with PATH_FILE_H being the capitalized relative (to the project src folder) pathname of the file. Any folder
separator character is replaced with the _ (underscore) character. This will ensure that two distinct files with the
same name won't share the same guard definition.
We also (might) define the FILE_H_INCLUDED macro as it is useful anytime we need to test if a core header file is included
(e.g. config.h or platform.h).
The pre-/post-increment operators are permitted and suggested, as long as the are used for some real shortcut/benefit in the code without adding unnecessary complexity or (indirectly) obfuscating the code.
That is, the are to be used in any idiomatic form like when pre-incrementing the index variable int he third part of loop
for (int i = 0; i < LENGTH; ++i) {
// Do something...
}Note that, in this case, we prefer to use the pre-increment (although this is more like an habit that a real benefit, as the compiler will optimize the code anyway).
They are permitted whenever their usage give some benefit to the code, such as when iterating over an array of pointers to gain access to the current item while moving the cursor, for example
const struct object_t *cursor = objects;
while (*cursor) {
const object_t *object = *(cursor++);
// Do something...
}When used to increment/decrement the value of a variable on a single statement the compound assignment operators should be used (that is,
+=or-=).
It's well established that even non OO languages can be used to implement an object-oriented approach to code.
In case polymorphism and abstraction is to be implemented (e.g. see the fs.h module) the preferred style is the
vtable struct approach. We defined a struct of functions pointers where the first formal argument is a pointer
to the object struct. In the object struct the v-table is the first field (so that sub-classes can extend
it while sharing a similar memory layout), followed by any required additional fields.
typedef struct Object_s Object_t; // Opaque type, we expose only a pointer to it.
typedef struct Object_VTable_s {
void (*dtor)(Object_t *self);
} Object_VTable_t;
struct Object_s {
Object_VTable_t vtable;
};
static void _object_dtor(Object_t *self)
{
*self = (Object_t){ 0 };
}
static void _object_ctor(Object_t *self) // Pass any other parameter, if required.
{
*self = (Object_t){
.vtable = (Object_VTable_t){
.dtor = _object_dtor
}
};
// Initialize the object fields, here.
}
Object_t *object_new()
{
Object_t *object = malloc(sizeof(Object_t));
_object_ctor(object);
return object;
}
void object_delete(Object_t *self)
{
self->vtable.dtor(self);
free(self);
}The constructor function is not present in the v-table, but it's defined and used to initialized the object structure.
We use OpenGL 3.3 core profile.
We could have used version 3.2, theoretically, but 3.3 is the first version unified OpenGL (that is the one in which the API and shader language match in version). Over the years version 3.3 ended in becoming the de-facto common-ground standard for wide adoption across pretty much every every "modern" card (i.e. produced in the last ten years). For this reason we are safe in using it. We could also target version 4.0 but the benefits probably would be few.
In the not-so-distant future the aim is to move to OpenGL/ES 2.0, which is almost identical to OpenGL 3.3 core in feature but ensure compatibility and ease-of-porting to browsers.
It's unclear whether OpenGL 3.3 core is supported on macOS. But then, we aren't interested in targeting the engine to Apple computers, for the moment being.
We need to (re)initialize OpenGL internal state several times during the composition of a frame. For example, we need to select the current texture, the shader program, the current VAO, etc...
Given the (relatively) simple use we make of OpenGL we could simplify this and select/activate some of them (e.g. the shader program) only once during the whole life of the engine. This would mean that the internal OpenGL rendering state will span over more frames. While this can appear tempting as a mean of optimization, can be source of difficult to trace bugs in the long run.
For this reason whenever we operate on the internal state for some reason, we also "clear it" by setting null/empty references. For example
glUseProgram(display->shader);
glBindVertexArray(display->vao);
glBindTexture(GL_TEXTURE_2D, display->vram.texture);
// ... draw everything...
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(0);
glUseProgram(0);Depending to its type, each shader variable has a different prefix:
i_for INPUT variables, that islocationvariables that are passed to the vertex shader during the drawing process (e.g. the pixel position);v_for VARYING variables, that is intermediate (communication) variables between the vertex and the fragment shader (e.g. something that is passed from a VBO to the fragment shader);o_for OUTPUT variables, that islocationvariables that are returned from the fragments shader (e.g. the final pixel color);u_for UNIFORM variables, that is configurable shader attributes (e.g. the current time).
Custom binary files are stored in a way that network-byte-order is (i.e. big-endian) is used to store information.
Since there's no straight native support for object orientation in Lua, everyone ends up cooking his own personal flavour of OOP approach.
While it's common practice (and basically the most natural way) to leverage the __index meta-method and meta-tables for that purpose, there's no strict policy on how to approach that. For example there's not a strict policy on how the constructor should be named.
In our codebase we developed an helper module for that purpose (see, tofu/core/class.lua). Also, the LuaX support library follows the same principle. The idea, specifically, is to have a overloaded new() method to create new instances.
However, while perfectly coherent this might end up being a bit "obscure" in terms of remembering the precise signature (number of arguments, types, and order) especially when different signatures have a fairly different outcome (e.g. create image from file vs. from a sized array of bytes). To solve this, we also USUALLY offer specific from_XXX() static methods that, internally, call the constructors.
Basically, the new() methods and somehow "private".
When a function exposes in its signature the idiomatic int nup argument to indicate the amount of upvalues passed to the callee we adopt the callee clears the stack policy. This is the usual Lua way of doing it and it means that in the following situation
lua_pushstring(L, "Hello");
lua_pushstring(L, ",");
lua_pushstring(L, "World");
lua_pushstring(L, "!");
process_words(L, 4);when the process_words() function returns the stack will be cleared of the four items that where pushed. This corresponds to the __stdcall calling convention and has the practical benefit that less boilerplate code (to clear the stack) is spread throughout the codebase, especially when a function is called in more that one point.
When implementing Lua OO code from within C we are classifying (and declaring) the methods in the following order:
constructors/destructors: this class includes, typically thenew(...)and__gc(...)methods (although the second one is formally a metamethod). However, any additional "creational" methods will be included, such asfrom_XXX(...)oras_XXX(...).metamethods: any__call(...),__index(),__len(...), and others will appear in this section. Despite being a metamethod,__gc(...)is included in the previous section as it is more related to the object lifecycle than to the actual behaviour.getters/setters: we are referring to getters and setters when talking about a single overridden method that, according to the call, act as an access or as a mutator. Usually they have to formXXX_v_v(), indicating that both the arguments and the return values are overridden.accessors: these are methods that gives insight of the internal state of the object without changing it, for example anis_XXX()method.mutators: differently from the previous, these methods modifies the internal state of the object without returning values.operations: this is the broader and less strictly defined class, as it include any exception to the other classes (i.e. a method that changes the internal state of the object and returns a value).static <type>: methods that doesn't operate on the object itself are grouped on a separate section. This despite being technically of some already grouped class (e.g.mutators), since we want to better highlight them.
This should be used anytime it feels suitable. However it is advised to keep it for the less time critical functions/methods.
As an example, we kept the Image.peek() and Image.poke() methods separated and avoided an overloaded Image.pixel() method that both gets and sets a pixel. This ensures faster access times.
We are implementing enumerated values as strings with a (case insensitive) value among a list of available ones.
Typically we convert the string value to an enum (integer) value. We conventionally assume and strive to make that there's an implicit one-to-one relation between a string and the matching integer value. For example:
// In the `input.h` file, the enumeration is defined as such.
typedef enum Input_Controller_Sticks_e {
Input_Controller_Sticks_t_First = 0,
INPUT_CONTROLLER_STICK_LEFT = Input_Controller_Sticks_t_First,
INPUT_CONTROLLER_STICK_RIGHT,
Input_Controller_Sticks_t_Last = INPUT_CONTROLLER_STICK_RIGHT,
Input_Controller_Sticks_t_CountOf
} Input_Controller_Sticks_t;
// Later, in the `controller.c` file, we are defining this mapping.
static const char *_sticks[Input_Controller_Sticks_t_CountOf + 1] = {
"left",
"right",
NULL
};This need a special care and attention, but permits to avoid an intermediate int-to-enum decoupling array. This is both an optimization in (code) space and (execution) time.
This, of course, requires that the enumeration starts from
0and proceeds incrementally w/o any "hole".
Since it's inception it has been possible to define Lua modules in an "hybrid" fashion, i.e. there could be a Lua file defining part of the module along with a C99 counterpart.
This had some limitations, 'thought, because the Lua code could only extend what the C99 code was implementing (for example with an additional method, see canvas.lua).
We have extended the support for this kind of "split implementation", however, and now it is possible also to implement the "core" module in Lua and selectively implement some methods natively. For this purpose in the C99 code the object need to be signature tested as LUA_TLOBJECT, which basically means that it is a Lua tables. Please note that there isn't a LUAX_LOBJECT(...) macro, because the object/table is manipulated through the stack and not by marshalling it to a C99 variable.
Beware! Accessing a Lua table from C is a potential performance hazard. Use this approach sparingly!
Another commodity for the hybrid module design is the post-load initialization method. Imaging you are creating an object with some userdata/metatable pair. Since the Lua module part is loaded and interpreted before the userdata and metatable are created, you won't be able to reference anything natively defined (for example, to create a method alias). The solution for that is the Module.__init() method, which is called (if present) at the end of the module loading process to enable some Lua-side initialization.
As a general rule, the style is very similar to what one would be adopt in any "modern" C/C++ codebase:
- Snake-case is used for the identifiers.
- Classes are in Pascal-case.
- Constants are all uppercase.
- Local module variables and functions are prefixed with the
_character. - NOTE: local constants have NO
_prefix, as uppercase variables with this prefix are reserved by Lua. - When including logic sub-modules, adopt a lazy-require policy, as some modules could be declaring non-trivial module-level variables. We might want to defer the allocation of such variables.
For further reference, please refer to this guide.
The required modules are the first thing to be written into a source file. They need to appear, lexycographically sorted, after the file comment-header. THe order of the modules is the following:
- engine module (e.g.
tofu.graphics.font), - game common modules (e.g.
lib.logic), - accessory modules (e.g.
palette).
It usually happens that some object are to be created at the very beginning of the game script initialization. An example of this is the default font, or the default canvas (to get the screen size). These are declared as <const> just after the modules inclusion section. Despite being objects, since they are immutable, the are also to be declared UPPERCASE.
This is typical example:
-- ... omissis...
local Vector2D = require("tofu.util.vector2d")
local Boid = require("lib.boid")
local Rules = require("lib.rules")
local PALETTE <const> = Palette.default("pico-8")
local PALETTE_FONT <const> = Palette.new({{ 0, 255, 0 }})
local FONT <const> = Font.default()
local CANVAS <const> = Canvas.default()
local WIDTH <const>, HEIGHT <const> = CANVAS:image():size()
local STATE <const> = CANVAS:state()
-- ... omissis...As a common guideline, the initial palette is declared as <const> immutable variable at the beginning of the main script file. Then, it's initially setup/configured in the Main:init() method, by calling the Display.palette() method.
Achtung! Don't use the main class constructor to setup the initial palette. The constructor of the class should be used only to initialize the game internal structure/objects. Of course, 'though, it will work flawlessly anyway. :)
As a convention we actually create two distinct palettes: one for the game graphics, and one for the overlay/debug text. We leverage the game-engine palette bank feature to keep them separate so that we can have a simpler drawing process. This way, we don't have to worry about the potential clash in color and re-indexing/shifting when drawing the text.
Following the snippet above, a potential initialization of a game can be the following:
function Main:init()
Display.palette(PALETTE, 0)
Display.palette(PALETTE_FONT, 1)
endIn the rendering callback, we will activate the second palette (bank #1) to draw the debug text, while the first palette (bank #0) will be used for the game graphics. For example:
function Main:render(_)
-- ... omissis...
state:push()
state:bank(1)
canvas:write(0, 0, FONT, string.format("%d FPS", System.fps()))
state:pop()
endThe canvas reference is never passed by the game engine, in the callbacks, as a formal argument. The underlying idea is that in the callbacks we are passing only the arguments/values that change over time (e.g. delta-time, ratio). The canvas never changes during the game execution, so it's not convenient to occupy/waste a call-stack slot (with all the book-keeping behind) for nothing.
It's more versatile for any script that needs to access it to get a reference to it with the Canvas.default() method. This, for example, can be achieved in the script header part (see above).
Of course, internal game scripts can behave differently and pass and use the Canvas reference (and any other, such as the State) as required.
Recently we added the support for native pixel-transparency (bit #7 in the 8-bits-per-pixel packed format). Along with the policy of having at most 128 colors in a palette we devised the following policies:
- images created as PNGs, transparency can be used without any issue, as the alpha channel is converted to the bit #7 of the packed format;
- Aseprite's color-index-remapping is encouraged to be used, to map the correct palette;
- fonts are NO LONGER two-color images, but single-color (typically white, but this don't really matter at all) with transparency.
- conversion to the packed format is done with
imagen; - mega-palettes can be achieved by packing palettes smaller palettes one after the other. During the image conversion, the
offsetoption inimagencan be used to "shift" the color indices and use a different portio of the palette for each image. For example, if we have a palette with 128 colors and we want to use the first 16 for the tileset, the next 16 for the player sprites, we can convert the tileset image withimagen --offset 0, the player sprites withimagen --offset 16, and so on.; - By leveraging the palette bank feature, debug messages are USUALLY drawn with bank #1 and (game graphics will use bank #0). This way we can avoid any potential clash in color indices between the two.