Saturday, 4 January 2025

XVE Game Engine : Ecs Tech & Update

For about two years I've had a home game engine project, this has just been a little sandbox in which I have been able to experiment with graphics API's, systems, memory, threading models and other such "Game Engine" like things; it has simply been a learning aid to keep myself invigorated.

You see I work on a AAA game engine, for a game yet to be released and that engine does a lot of things its way, tens of engineers work on it in the "Engine" space.

I therefore wanted a space of my very own in which I could explore, the key piece I have done which ape that real engine have been using an Entity Component System.

My Ecs is quite different from the one at work, my focus is on game objects being made of many entities and those entities carrying discrete components to add their functionality.  A very data driven ideal.

A great example might be simply putting a model on screen, lets take a complex object.  The Missile Launcher as a case study and we will be looking at the code before images from the running application.

 To place the game object entity I create the Object structure, filling it out and assign it a transform and launcher state instance, these components give the engine a key into the nature of what this entity is.  There are many explanations of Ecs out there, but simply put we get an entity handle (an Id) and assign to it certain component types.  The presence of these components then tell us everything we want to know about the entity:

components::Object launcherObject
{
.mId = GetNextGlobalObjectId(),
.mDebugName = "Launcher"
};

components::LauncherState state;

components::Transform3d launcherTransform
{
.mPosition = { 4, 1.5, 4 },
.mRotation = { 0, 0, 0},
.mScale = { 1, 1, 1 }
};

This could be all we need for the entity, indeed this is a design decision I am yet to fully explore.  However, in the actual code I immediately want the base model for the renderable to have this same transform.

Adding this is as simple as assigning more components:

components::Renderable baseRenderable
{
.mMesh = GetMeshIndexForName("Launcher_Base"),
.mTextures { GetTextureIndex("swatch.png") },
.mVisible = true,
.mBlended = false
};
components::Shader shader
{
.mShader = GetShaderIndex("Brambling1"),
.mWireframe = false,
.mLighting = true,
.mFog = false,
.mCameraRequried = false
};

The renderable and shader component are very strong indicators of quite how this works, an entity without these components will not ever draw.

In fact there are three components needed for the "Draw" system to pick up the entity and run the graphics API calls over the entity, these are the Transform, the Renderable and the Shader components; these three together indicate that this is something the engine will draw.

Systems in my engine do this with a view:

xve::ecs::View* mRenderableView{ nullptr };
mRenderableView = CreateView<components::Transform3d,
components::Renderable,
components::Shader>();

And the system simply iterates all the entities in this view and we have just those entities to show.

(At this point I know a bunch of you are lost, but lets just think of this like we've divided the thousands of entities in our game such that only those with these three entities will appear in our View).

We have right now though just got the object itself showing and the model showing is only the base, I want to rotate this base.... I have a component for that:

components::ManualRotationY manualRotationControl;

Assigning this component to the entity and the functionality this component brings is assigned to our launcher!

This is the key power of an Entity Component System, we can author discrete and well defined features as a component and which operate on the member data within that component and then assign it to any other entity we desire.  Manually Rotating the entity around the Y (or up axis) is a good example.

Another might be the speed of a vehicle.

We however want to add a body model to our launcher object, our second model looks like this:

Adding a child entity to this launcher object we can keep all the components separate for the body than the base:

components::Transform3d bodyTransform
{
.mPosition = { 0, 2, 0 },
.mRotation = { 0, 0, 0},
.mScale = { 1, 1, 1 }
};

components::Renderable bodyRenderable
{
.mMesh = GetMeshIndexForName("Launcher_Body"),
.mTextures { GetTextureIndex("swatch.png") },
.mVisible = true,
.mBlended = false
};
components::Shader bodyShader
{
.mShader = GetShaderIndex("Brambling1"),
.mWireframe = false,
.mLighting = true,
.mFog = false,
.mCameraRequried = false
};

components::ManualRotationX tiltControl;

const auto body{ CreateEntityWithParent(launcher,
std::move(bodyTransform),
std::move(bodyRenderable),
std::move(tiltControl),
std::move(bodyShader)) };

Because we have added this second renderable as a child (the parent being the launcher itself) the transform is hierarchical and the launcher now looks like this:

We want the launcher to carry missiles, but where to place them?  Well, I have invented a "socket" system, that when you place a socket entity onto something you can find the socket and insert something into it...

std::array<glm::vec3, 4> missilePositions
{
glm::vec3{ -1.2, 0.5, 0.5 },
glm::vec3{ -1.2, 1.0, 0.5 },
glm::vec3{ 1.2, 0.5, 0.5 },
glm::vec3{ 1.2, 1.0, 0.5 }
};

for (uint32_t index{ 0 }; index < missilePositions.size(); ++index)
{
components::LauncherSocket socket
{
.mIndex = index,
.mPosition = missilePositions[index]
};

launcherState.mSockets[index] = pRegistry.CreateEntityWithParent(body, std::move(socket));
}

 And we can add new child renderables to these sockets, pointing to the missile model:

This activity is performed in my "LauncherUpdateSystem", lets look at the whole "Update" function:

void LauncherUpdateSystem::Update(const xve::base::time::GameLoopTime& pDeltaTime)
{
auto& registry{ GetRegistry() };

for (const auto& launcher : *mLauncherView)
{
// Get the state of the his launcher
auto& launcherState{ registry.GetComponent<components::LauncherState>(launcher) };
if (launcherState.mSecondsSinceMissileAdded < launcherState.mReloadTimeSeconds)
{
launcherState.mSecondsSinceMissileAdded += pDeltaTime.GetCurrentDeltaTimeS();
}

// For each socket
for (uint32_t index{ 0 }; index < launcherState.mMissiles.size(); ++index)
{
// if there is no missile in this position
if (launcherState.mMissiles[index] == 0) // zero should be null entity
{
if (launcherState.mSecondsSinceMissileAdded > launcherState.mReloadTimeSeconds)
{
// Add a missile to the matching socket position
AddMissileToSocket(registry, launcherState, index);
// Done adding missiles as we just reset the time
break;
}
}
}
}
}

We can see that for each launcher in the Launcher View (which is just any object with a components::LauncherState) and we know we can pull out that component and work on adding a missile if the launcher timeout has passed the reload time; calling AddMissileToSocket you can imagine looks for children of the launcher which have the components::LauncherSocket component type, and voila we have the transform from the base, the body and the socket to place the missile.

Putting all this together and we get:


You can see the component have their own UI (ImGui) controls, that one has complete control over the whole entity and each individual component.

And the parent-child relationship between different entities within the object allow for really complex game objects to be described in fairly quick turn around times.

Another example of this is my test vehicle, which has a rolling wheel to match the movement; the movement belongs to the vehicle, which is a game object with a renderable.  But the wheel itself is a child, it has a renderable too, but also its own transform and its own "Wheel" component, which has the discrete maths for the circumference and rotation.

The effect of rotating the wheel per frame is even its own other component, so that we can assign the same "Rotate per frame" component to anything in our scene.

With all this technology in place, the basic rendering engine as you can see (in OpenGL 4.3) I am now placed to expand the amount of content and start to build an actual game.

You see a game engine, or any engine, can only really fulfill its potential and for me complete the learning round trip by creating and driving something, for me this will be a game.

The missile launcher is perhaps a hint as to the kind of game I am picturing. but the models I am working on as of new years night are perhaps stronger clues:


The Entity Component System has been a key implementation for my to progress onto content creation and, besides a few performance fears when I scale up the combat scenes I hope to deliver, I believe it to be the best choice for fast and easy iteration on a theme.

The mental leap of a game object being one or many entities, each with their own features dividing up the mammoth task of implementing all the features of a game for myself as a solo developer has been absolutely key.

Wednesday, 23 October 2024

Read the Screen

We are at the change of am epoch, moving from a time where we would mockingly tell someone to read the manual when they became stuck to now having to literally instruct people to read the screen.

 "READ THE SCREEN!"

If it is not an in your face, center of screen obscure everything until you dismiss it; or worse still a ping followed by a side of screen chat bubble; then users simply do not engage with it.

Time and time again issues arise where some user declares they are stick, some player derisively mocks a game as hard, or bad, because their little monkey brain did not tell them to read the screen.

Away from computer screens, apps and games, in the classroom their are standards to be met in fluency, fluent readers just see the text and can not but help to read the text.  One can forgive none fluent readers, or non-native speakers, when they struggle in this regard.

However the seeming trend one can observe is for native speakers of the language software is delivered in are unable, or unwilling, to engage and read the screen.

This trend seems doubly emphasized in gaming, where modern games design bread crumbs and way markers and all sorts of mechanisms to point the player to a conclusion or action.

Gone are the games of leaving the content and the player to mull over the opportunities, the sandboxing, in a game world.  There can be no more sandboxes, you can not leave a player washed up on a beach and expect them to work out all the mechanics and order to survive:

Tom Hanks makes fire for the first time | Cast Away | CLIP - YouTube

They are simply unwilling or unable to do so and instead opt for any game play sequence which achieves their aims and that internal self-satisfactory dopamine hit on achievement the quickest.

I believe the major driver to this change in play style is an absolute lack of willingness to engage with the screen and especially players avoiding reading.  The reading comprehension levels have dropped, I can make assumptions that some of this allowed drop in prerequisite levels can be draw between who is playing games along with the kind of games being played.

I do not believe anyone can deny video game players have taken this turn.  And in the market space it is understandable, if frustrating for those of us wishing for a more engaging and interesting game play loop.

Game makers have had to follow the trend to maintain the mass number of players and major titles, they can not and do not take the risk of making a game which is too hard, or has too high a cost of entry.  They want as many customers as possible directed to the short cut the masses to the feeding trough.

The recent vogue of playing World of Warcraft Classic in Hardcore mode is perhaps a phenomenon we can point to where players sought their own meta, to make the game harder and more engaging and I believe more fulfilling because it broke this trend.  If you played badly you died, you were not rewarded, if you did not engage with other players you died and you got them killed, you were ostracized not rewarded.

A literal case where if you were not up to spec you were actively nulled out of the pool, as perhaps Darwin Theory states nature should be.  It was refreshing, if nerve wracking, to see.

Beyond large new titles or established franchises indie makers are a little more able, and some of the most amazing experiences come out of that style of play, a style of play today considered niche, we must remember many of the game play tropes considered niche today were once mainstream.

MadseasonShow: Lifelong WoW Player Plays Old School Runescape
 

I remember fondly feeding coins into a machine to keep playing and death reset you to naught; without wishing to raise a tangential thought too eagerly, I would highlight that feeding coins into an arcade cabinet was the original micro-transaction driven game play loop and I'm surprised it isn't leveraged more widely today (it may very well be and I'm simply ignorant).

All this concern in the drop in seeming fabric of the game play experience all seems to me to stem from the audience attention spans having dropped, memory skills have dropped, social skills having dropped and crucially language skills have dropped.

We no longer interact like group oriented beings, even in team forming experiences, too many games proffer group finding, raid finding and instant way to find two, four or thirty players you hope to maybe mold an effective fighting force out of and it does not work.  I personally believe this is yet another tangential thought, but it is closely related to this degradation in attention and communication skills, as group finding actively assists such lone wolf selfish players to remain competitive, if not dominate, a play experience.

Saturday, 13 July 2024

What is it with the AMD Adrenaline Software?

Very quick one, I've taken delivery of a new Graphics Card... (oooooo ahhhhh).

And it's a seed change for me as this is my first ever AMD graphics card, so this is of course going in my own main PC, which itself is my first ever AMD CPU powered machine I am therefore all Team Red for the first time ever... EVER!
I have been so excellently impressed with the performance of the Ryzen Zen 2 chip at the heart of my machine and my old EVGA 1080 GTX Superclocked was showing its age.  It was therefore time.

Unfortunately I made a huge mistake... No, it wasn't the model, no it wasn't the price, no it wasn't even paying to import it (because it was cheaper by a margin to have the card fly all the way to me through customs from California than actually buy it here in the UK - Thanks Brexit! - Not).

Anyway, it arrived and I plugged it in, and my monitor didn't come to life; I then found the signal was going to my massive 4K TV (on the HDMI) not my monitor (on the display port - go figure).

That sorted I then needed drivers, so I went to the AMD site and looked and found what it said was the best package to install on Windows 10 64bit for this card....

I don't know ANYTHING about the AMD software, and oh boy is that a mistake.

I installed it and was immediately REALLY REALLY IMPRESSED with it!  Yep, it's beautiful (even if it is backed by Qt and I hate Qt) and smooth and does wonderful things.

Unfortunately, and this is key, it was taking a lot of CPU whilst doing this work... So I closed it... and it STILL took a bunch of CPU, nearly 10%!

Just idle at the desktop, 10% CPU.

I was properly baffled by this, it must be doing something, so I went through all the settings and disabled everything I could see, everything turned off, clean reboot, and it was still taking 10% but now was peaking to 14% on odd occasions just idle!

What the heck, a bunch of goodling and I can find a bunch of folks complaining about exactly this, and one guy properly threatening to boycott their brand unless they explain how to just install the drivers without this software suite.  There was no answer to these please, and they were old posts, like there's been an apparent hard reset of the search results for this kind of question.

I spent a bunch of time trying to fathom this issue to no avail, so I like my forebears in the search results set about trying to find JUST a driver package.  I could not find one,

I therefore just decided to pull the card and possibly return it.  And so uninstalled the AMD Adrenaline Software, it took its time... And really grated on me as it spent about two minutes showing me the text "We value your feedback"... without ever giving me a link or contact in order to tell them anything... and this is during an uninstall process; surely someone when they added this eye stabbing annoying message thought "Oh we need to have them able to give me feedback!" ... Nope, seems not.

Now completely uninstalled and the package opens a website... This website... which I link only for you to see... as it's an advert... for a version of the very card I am uninstalling the software for.... Yes, AMD that's pretty tone deaf, also... WITHOUT YOUR DRIVERS INSTALLED YOU CAN NOT VIEW THAT SITE, so double double own goal there .... Less Advanced Micro Devices and Anyone Might Despair.


Here I am then, flabbergasted, back to a tiny resolution, opening chrome and trying to find just a raw driver package and I can't find one.  My machine has now cooled and I hear the CPU pump has ceased whirring away, as it was constantly with the 10% load on it!

I sat looking for ages when I first installed the card, really ages, we're taking an hour before I went with this Adrenaline stuff.

When suddenly Windows itself kicks into life and installs the Microsoft supplied WHQL driver, and guess what?  Yes, it's the same base driver, and it works absolutely brilliantly... and crucially my machine is not taking any CPU when idle, it's gone quite quen idle, I'm sat now typing this with a YouTube video playing, about 6 chrome tabs open and a copy of Visual Studio Code open too and it is silent, the machine is silent, just as I built it to be (unless under load).

Looking about I see AMD are recruiting software engineers, I have no idea what for, but if they see these pages - I'd suggest a bunch of refactoring is due over there folks.

Tuesday, 9 July 2024

Code Locality & Concurrent Systems

Let us talk Software Design, let us talk about code locality.  What do I mean by locality?  Well, in Software Engineering we often talk about code being self documenting, a fabulous place to be if the code performing the work is right in front of you.  But to be honest systems get pretty big pretty quickly.  So you're very much more likely to me making calls to API's or just bunched of functions you have to blindly trust, unless you have the leisure of digging into them.

And there's usually precious little time in development which is invariably taken up with writing new code, not going over the old (unless you're very lucky - but that's a conversation for another day).

Now, these API's can encapsulate large features, and they don't always achieve the amount of descriptive power we'd like in the call site location we're at.  I therefore advocate for a comment around that point.

And so we are immediately at odds with the wish for our code to be as accessible, local, as it can be, but also encapsulating the large unrelated tasks elsewhere.  For me this is the dichotomy of code locality in a nut shell.

Where can it come unstuck?  Well, back in the day (and I have to be honest with most engineers still thinking linearly even today) you could be forgiven to writing out your big system block diagram, connecting things with events or signals and just going about your business, when your code called "BigFooFunction" in "BarBarBlackBox" library you didn't pay it much mind.  Today however, as Moore's Law runs aground on the rocks of pumping ever more cores into a system we have to think in concurrent terms and it is in just such a scenario that I want to pick up thinking about Systems Design and Code Locality.

Let us perform a thought experiment.  We have a system which progresses from Eggs to Birds, it controls the state of the entity laid as an egg, being incubated, then hatched, fed and tended until they start to fledge and ultimately turn into a bird.  All this transmogrification of the state from Egg to hatchling to fledgling to bird happens asynchronously in the background in a big slap of system you do not have to worry about.

All you worry about is a signal coming into you saying "predator", and when this signal arrives you need to stimulate all the little birds you have into action, they all need to take flight.

for(auto& bird : flock)

{

   bird.TakeFlight();

}

This is the locality of our change to the property flight on each bird, we are absolutely unaware of each individual possible state the bird can be in and so we rely on the API and code backing our call to "TakeFlight".

Now, let us assume the members of "flock" are all of the base type "Bird", so they all have a TakeFlight function?  Well, they might, if Bird looked something like this:

class Bird

{
   public:

       virtual void TakeFlight() = 0;

};

Then at compile time all the derived classes would have to implement "TakeFlight".

class Egg : public Bird

{

    private:

        uint64_t mTimeLaid;

        float mTemperature;

    public:

        void Incubate();

        void TakeFlight() override;

};

And because we know this is an Egg we know it can't fly and so we know that this override of the function will do nothing.

This is perfect code-locality for that derived type, but for bird itself it leaves us the open question, well what does it do?  Does it do anything??  Should "TakeFlight" return us some code to indicate the bird took flight and an error if not, but an egg not flying is not an error, so must it be some compound it returns.

Now, I am straying into API design somewhat, and they are related fields, but really here we're thinking about where the active code sits, what is its locality compared to our callsite in the loop?

And for a function, you can see we can define this and know.

Now lets us change our example:

class Bird

{
    public:

       bool mInFlight { false };

};

and

class Egg : public Bird

{

    // As above

    public:

        void TakeFlight () override { 

            // Intentionally Empty 

        }

};

Our egg can not fly, or can it? for now with a public member we're flying somewhat in the wind of a contrived example, but anything can now set the value of mInFlight, our for-loop upon the predator signal can now achieve its aim:

for(auto& bird : flock)

{
   bird.mInFlight = true;

}

And this is correct, this loop in review would pass, who can argue?  The bird took flight, it did, it is true.  And this is code locality in action, for at the location we needed it the functionality was available to mutate the value and achieve our goals, no matter what the state of the rest of the system was.

This is a very dangerous place to be.

Especially with an asynchronous system, lets say this is not a trivial call, lets say that the call site is to loop through a series of resources and start them loading, and upon that call each is pending load, but not yet loaded?

for(auto& item : objects)

{
    item.StartLoad();

}

We can assume this code is starting the load, but now lets package this into some context, we like to have our code be self-documenting after all.

class Loader

{
    private:

        bool mEverythingReady { false };

        std::vector<Objects> mObjects;

    public:

        void Load()

        {

            for(auto& item : mObjects)

            {

                item.StartLoad();

            }

            mEverythingReady = true;

        }

   };

Can you already spot the problem?  The local code here is communicating that EVERYTHING READY, when it is anything but that, you have simply started some other action elsewhere, you have not checked the state, you have not deferred until ready, you have started load and that is all you know, but you code here locally is communicating something subtly different.

And in huge systems you must not fall foul of this kind of behaviour, you need your code locally to communicate what it intends, to do as it intends and if you spot silly public interfaces like this do not be affraid to fix them, the bravery to address an issue, if only to raise it to the owner, is a step in the right direction with massive software systems.

Monday, 1 July 2024

Operational Embarassment

Today I bring you a story, a true story, from my very own past.  You may have seen the "broken feet" link on this very page, so yeah I broke both my ankles at the same time falling off a roof.

Fast forward just under two years and I need an operation as the joint are going bad.

On the day of the operation I am handed a lovely double hospital gown and the lady checks who, what and why I am there... The surgeon see's me and puts a big big black arrow on my right leg and I am shown into the pre-op waiting room to just wait.

I am the first case, but they want to pre-op everyone, and slowly the waiting room fills with men in the same garb.

No-one speaks.... No-one.

This is quite strange with us blokes, and to be honest I find it a little freaky, as I'm sat there with my big black felt arrow on my ankle I just joke... "Bet we can guess what I'm having done".

ALL OF THEM, and there's like six guys now, all stony faced.  They don't look at me, they don't look at one another.... Nothing, nada, zip.

What the heck?

So I'm up first, off I go, limping out and am taken to theatre.

I am a quirky case, as not only am I first, but I am to be awake during the procedure, I get an epidural and then a tourniquet is applied and I get to watch them work on my ankle in this freezing cold room.

An hour or so later I'm in resus, first in, first out... I am surrounded by empty bays.  Slowly as the morning wears on and my leg starts to awaken in cramping pins and needles the first of the other guys is brought through... No idea what procedure he's had.

Then another.

Then another.

Until all six bays around me are full and I'm not allowed a drink, some toast and encouraged to rub some life into my leg.

I am very clearly the only patient in this state, the others are all coming around from general anesthetics.

My surgeon pops to see me, he's very happy, I'm very happy and sure enough in the next hour I get up and for the first time since the original accident I'm not in pain in my ankle!

These other guys then start to awaken, and they don't want the nurses helping them, the female nurses, they're all deadly coy and a bit embarrassed, as I'm up and about moving my leg and getting it wrapped and a cast applied.

 "Sorry"

This voice comes across the ward.

 "Sorry for ignoring you earlier, you were just breaking the ice"

And we have a chat..... And then he drops me into the most embarrassment ever....

 For you see... he... and seemingly all the other men in the ward... have all had dick operations.

 Yes, that's right... In a room full of men about to have their john-thomas tucked or worse I made a joke about "Guess what I'm having done".

I wanted the floor to open up and swallow me.

Everyone then promptly burst into laughter at the look on my face.

A couple were having later life circumcisions due to tight foreskins, one was having a lump removed, one was just have it biopsied or something... but all dick ops...

That's why they were silent and jealous of me with my big black felt marker arrow marking me out as the luckiest bastard in that room with the least to worry about.

I think about this today, my big black felt arrow, never have I been more relieved after hearing their procedure stories.  Though they went a little pale when I said "Oh I was awake during the op and watched".