2024-02-25

Project Dusk #4 - Testing scene graph webbuild 2

Here's the newest playable build of the scene graph test:

The complexity of the scene has grown significantly. As always, I have difficulties to do game design tasks and naturally focus on world building stuff - like making the ground look nice. The plane controls are untouched compared to last time and there are now only a few shootable targets but without any effect on anything. I think I want to make a game in style of "Blue Max" from C64. One of the first games I played (when I was 5 or 6 years old). Couldn't figure out anything, but the game left an impression. It's simple enough for such a test too.

So what all did change?

Scene graph

One primary purpose is to test the scene graph performance. You can toggle a stats overlay when pressing "I". There are 500-1000 entities in the scene because of all the trees and performance wise, it looks quite good; the most expensive part is the rendering - since I don't use any instancing or anything, it's just a lot of individual draw calls. Optimizing this is right now not a priority - also because it's still limited by VSYNC (which I can't disable on my system somehow).

The scene graph is now also capable of handling a hierarchy of objects. The trees are all children of a single parent object. The parent object is moved around and the trees follow. Moreover, deletion of the parent object also deletes all children, which seems to also work.

As laid out in the previous log, I primarily want to test performance, but usability is also a factor. In regards of that, I consider this already a success: It's really easy and effective to use. The API is simple and straightforward. There are some functions missing though; Mostly helpers such as transforming world into local coordinates or keeping positions when reparenting objects. The difficult part is to convert the matrix into the base values that I use for object positioning (Translation, Rotation, Scale). I've done that with the raygine editor, though it was buggy for certain cases. I will have to revisit that.

What I find outstanding is that it's fairly easy going to work on this prototype in pure C. I miss the instant reloading, but the C code complexity regarding memory management and pointers is much lower than in my previous attempts. This is due to the way how I am now managing references; the offset + version approach is very simple and effective. Especially handling the case when an object gets deleted is effectively solved. Let's look at the auto deletion component:

static void AutoDestroyComponentUpdate(SceneObject* node, SceneComponentId sceneComponentId, float dt, void* component)
{
    AutoDestroyComponent* autoDestroy = (AutoDestroyComponent*)component;
    autoDestroy->lifeTimeLeft -= psg.deltaTime;
    if (autoDestroy->lifeTimeLeft <= 0) {
        SceneGraph_destroyObject(psg.sceneGraph, node->id);
    }
}

void AutoDestroyComponentRegister()
{
    psg.autoDestroyComponentId = SceneGraph_registerComponentType(psg.sceneGraph, "AutoDestroy", sizeof(AutoDestroyComponent),
        (SceneComponentTypeMethods) {
            .updateTick = AutoDestroyComponentUpdate,
        });
}

The registration step provides the information how the component is managed and what functions it provides. A component without an updateTick callback is not going to have any impact on the update step logic. The updateTick callback is called every frame and is provided with the scene object and the component data. In this example, the remaining life time of the object is decreased and when it reaches 0, the object is destroyed. It looks simple, but in many systems, this behavior requires the system to handle this situation correctly. Any component could delete anything at any given time. Usually this is causing lots of headaches. In many systems, the deletion is queued and then executed after the update loop(s) have finished. In my system here, this isn't needed: Any reference to any of the deleted objects is instantly invalid. The only requirement is, that no component retains hard pointers to other components or objects. As long as this rule is followed, the system is very robust and simple.

Ground tile system

The ground is rendered using tiles on which trees are placed. It is continously generated while flying over the terrain (pressing 'q' can speed up the ground scrolling). As a basis, I am using a grid of vertices, where each vertice has a type that is either grass or water. I am determining this type by doing perlin noise calculations. For 4 vertices, I find a fitting tile and rotate it into a fitting position. This is a simple process that I want to describe in detail in a future log. For the trees, I am calculating a value that is based on the interpolation of the four data points and it is being then modified using another perlin noise calculation. I am using the value also as input value for the size of the object.

The more interesting part here is for my test is not the visual quality but the complexity of creating larger amounts of objects, placing them in a hierarchy and deleting them altogether again. The performance seems to be ok. Again, pressing "i" will toggle the information overlay and "r" will toggle the rendering calls, which is useful to test the performance impact. Right now it seems to not make much of a difference, the vsync limit is the bottleneck.

Debugging info:

🍪