🏠

Discord. Models. Releases.

2024-08-04 / Mark Sowden


← 2024-07-212024-08-18 →

Discord

One of the things I tried to look into since the last post is Discord integration. This was utilising their Discord GameSDK.

Upon calling DiscordCreate, something was failing in the library, and it was just calling exit. Why bother having a whole suite of return codes if you're not going to use them?

Passing DiscordCreateFlags_NoRequireDiscord per the param data allows it to get a little further. According to the documentation, this allows it to work without Discord running. That seems reasonable. And, oh hey, it says "[...] use this on other platforms". Okay.

So let me get this right, the default create flag requires Discord to be running. Otherwise, your app will just exit without any error. This is despite the fact that there appears to be a DiscordResult_NotRunning return code. Great design guys.

But alas, now we've got an unhelpful DiscordResult_InternalError code. So again, despite having a whole suite of return codes, apparently this is failing in such a way that it can't give me a more helpful clue. On a whim, I decided to reinstall Discord via the Debian package rather than Flatpak; this fixed it, and it was able to successfully find the Discord client.

Discord integration working.

So that turned out to be more painful than it probably should've. Originally, the implementation was going to go into the engine, but I feel like it makes more sense to throw it in with the rest of the game code.

Models

So this one has been in the pipeline for way too long. My brain decided to keep making things as complicated as possible and to keep adding new features before I could finish others. On the 25th I basically said enough is enough, grabbed a model from Team Fortress Classic to use as a placeholder and just tried to get it done.

Initial results weren't so great.

Screenshot showing an, uh, abomination.

Fortunately, that was quickly resolved.

Screenshot showing the model looking almost as it should.

Looking better! Lighting here is a little messed up but that was fixed later.

There are still some issues to solve here. An obvious one is the lack of shadows. Shadows for the world geometry were "easy" (though still need further optimisation), but shadows for the models need to work a bit differently. And skeletal animation is still pending.

The pipeline to get a model into the engine works like so for now.

First the model needs to be added to the cook config for the given project (note the "placeholders/p_char_a" under models). Technically, I've allowed you to explicitly just run a command through the cook tool, but this is my recommended route.

[...]

object cook
{
    ; here is a list of properties for the cooking tool

    array string worlds
    {
        ; list of worlds to process go below (operates under 'project/worlds/')
        "zoo_shaders"
    }

    array string models
    {
        ; list of models to process go below (operators under 'project/models/')
        "placeholders/p_char_a"
    }

    array string materials
    {
        ; explicit list of materials
    }
}

[...]

Following this, you'll then have to create a config for your model at the location you specified. In this case it would be placeholders/p_char_a.cmf.n. This works very similarly to how QC files for Valve's own tools work, as you can likely see.


    node.utf8
    object cookModel
    {
        bool isStatic false

        float scale 0.2

        string name "p_char_a"

        string body         "characters/soldier/soldier.smd"
        string materialPath "models/placeholders/characters/soldier"
    }

This will naturally become less involved as the editor gets fleshed out more.

The actual loaders here are abstracted out, so if you wanted to support something other than SMD for your geometry, that's totally possible (I'm already looking into PSK/PSA support).

Internally, the model is then handled through several phases. First, the geometry gets loaded via the loader interface, which loads and parses the file into its own representation specific to that format. Then that gets converted into a shared in-memory representation of our own model format. And finally that gets serialised into a binary mdl.n file that is thrown into the ship directory.

I might do a video at some stage talking about how all this works in a bit more depth, and maybe talk about some other things such as the material system.

Automatic Cooking

A messy kitchen.

Source / License

I got a little bored with running the cook tool each time to check things out. Technically, I could've added a script to do it, but I'm... Lazy?

So, nothing too exciting, but when launching the game, you can now pass a /cook argument which will cause the cook tool to run before the game actually opens up. This should make it a little easier when making alterations outside and then wanting to quickly check them in the engine again.

Right now this takes less than a second to run, but as more and more content goes in, I've already got some improvements in mind for speed.

Further Optimisations

Previously, if a light enveloped an object, it was assumed the whole object would be lit. This works okay for smaller objects but not so much for the large geometry of the world.

When drawing the world, the engine will now ensure that the pass for that given light will only include the faces the light is actually hitting.

Screenshot showing faces that are being hit by the light highlighted for debugging purposes.

This doesn't generally yield much of a change in performance for the test environment I've got here, but it's obviously going to pay off when we're dealing with larger environments.

This optimisation only applies to omnidirectional lights for now.

To Shadow, or Not Shadow?

Screenshot showing the model looking almost as it should.

So despite all the improvements I've made to shadows of late, I've been a little on the fence as to how extensive this game is going to make use of them. Visually, my goal is to aim for something really simple looking - a constant reference I've got in mind is Spyro the Dragon.

Right now what I'm thinking about doing is using shadow volumes for characters / objects, probably for anything considered "dynamic" I suppose, without self-shadowing. And then vertex shading for the environment, and possibly implementing some sort of lightmapper that operates per-vertex.

Translations

There's now an API for translating strings to other languages provided within the shared game framework.

Screenshot showing some uh, almost German text.

Adding support for new languages is fairly straight-forward. There's a scripts/strings.cfg.n file you'll need to provide which it will attempt to load on startup.

node.utf8
array object languages
{
    ; id            Internal identifier
    ; description   Name used per interface

    {
        string id           "ger"
        string description  "Deutsch"
        array object strings
        {
            {
                string id       "test_message"
                string value    "Das ist ein Test. Hallo. Ich spreche auf Deutsch. Es ist großartig!"
            }
        }
    }
}

As you can see, you just then need to provide an entry for the given language with an internal identifier and a description, and then the actual list of translations.

In most cases, the id for a string will just be the default (i.e., English) version of the string. However, I've allowed for setting an explicit identifier if desired just in case there is awkwardness in translating relative to the context.

The actual internal API looks like the following.

[...]

typedef struct GameLanguage
{
	char id[ 4 ];
	char description[ 64 ];
} GameLanguage;

/**
 * Returns an array of all the available languages.
 *
 * @param num 	Output number, representing the number of elements in the array.
 * @return 		A pointer to an array of GameLanguage instances.
 */
const GameLanguage **game_language_get_available( unsigned int *num );

/**
 * Sets the current language based on it's internal ID.
 * If the ID is specified as null, it will always use the fallback.
 *
 * @param id	Internal ID of the language desired.
 */
void game_language_set_current( const char *id );

/**
 * Looks up the given string by it's ID, and if it's not found, returns the fallback.
 *
 * @param id 		The ID of the desired string.
 * @param fallback 	The fallback if it's not found.
 * @return 			Either the translated string or the fallback.
 */
const char *game_language_lookup_string( const char *id, const char *fallback );

// A macro just for ease...
#define G_STR( ID, FALLBACK ) game_language_lookup_string( ( ID ), ( FALLBACK ) )
#define G_STR_( ID )          game_language_lookup_string( ( ID ), ( ID ) )

[...]

This is one of those things where I can hopefully just wipe my hands of it now.

Public Releases

My long-term plan for the engine was to eventually make the whole thing public. I even went as far as creating a website for this (which is technically live, I've just not advertised it).

What I've trended towards instead is making older milestones available, with full source code; a year later - this is what I've already been doing for the last few years.

But as I've observed other projects over the years, my appetite for making some "current" version of the engine actively available isn't as strong as it used to be. I really don't want to take on the responsibility and frustration of that. This mindset has definitely been accelerated by my experience with Jaded, which totally took the fun out of that.

The engine is already pretty modder friendly, and I think support for Lua will open things up a bit more whenever I've got time for that. But when (or if) this game goes out there, besides the odd documentation I've thrown together, I think I'll probably vanish into my hole and leave people to it.

Essentially the Valve approach, I suppose.

Anyway, long story short. Unless there's another game I release with the engine any time soon, I do not think I'll be making the source code available again for some time.

WebGPU

I've been thinking about it on and off for a little while now, but I've been somewhat tempted to update the renderer to utilise WebGPU, via the wgpu implementation. Although I'm not fantastically familiar with Rust.

I realise WebGPU isn't yet mature, but it seems good enough as far as I can tell, and I'm not expecting this game to come out for a few years yet anyway.

Google has its own implementation called Dawn, written in C++, but that's Google.

That said, I've been going back and forth on how to best do it. Right now everything uses my plgraphics library for rendering and handling abstraction. My choice is to either add WebGPU support there — which is a little odd given wgpu can then wrap that to multiple APIs itself — or I replace plgraphics with WebGPU entirely.

I'm leaning towards the latter. It's not urgent regardless, so I've got plenty of time to think about it.

That's All For Now!

So that's it for now. I've got a pretty good flow of these going now. Whenever anything pops up, I just immediately document it, and I'll continue trying to do that.

Wasn't sure where else to put this, but here's a crappy flare attached to a light attached to a rope.

And as mentioned before, now that I've got a concept running I'm happy with, hopefully I can continue documenting this process from start to finish.

If you've got any thoughts or feedback, feel free to reach out to me.

Until next time, keep happy and healthy!

← 2024-07-212024-08-18 →