Back to the blog

Mach (Zig) Adventures - Part 3

Click here for Part 2


There are some simple things I would like to accomplish in this session. It would be fun to see an NPC randomly moving around on screen. It would also be fun to see some basic player controls moving a player around the screen. I would love to have some basic collision detection so the player cannot walk over the NCP and vice versa. It would be even cooler if I could create some arbitrary blocked off positions in the world map and have the NPC and Player be forced to respect those positions too.

This probably means I need to bring in my sprite2d example from the mach-examples repository. I want to use the one in my PR because it has some advanced rendering operations there that I want to take advantage of.

The Plan

My sprite2d example uses imgui, JSON parsing to parse a JSON file of data, a spritesheet (atlas) for sprites and imgui for no particular reason other than I wanted to see if I could use it at all. The issue I am immediately going to run into is that some of the assets I need are loaded by Mach from an asset folder in a specific path and then embedded into the application.

I will start by trying to get the sprite2d example working with my source code. Then I will start by expanding my existing structs and merging that work with the sprite2d example wherever necessary.

Since imgui will be available (if I do this right) then I may as well use it. I can use it to display some stats or messages or things like that. I know that you can also get imgui rendered on the web, iOS, Android and a few others places, which hopefully means I can get this rendered everywhere when Mach gets that far.

If I cannot use imgui everywhere for some reason, that's ok. Mach is going to let me swap out imgui for something else like Tsodings Olivets cpu based renderer if I want.

In the end, I hope to have imgui show me the current position of NPC, the Player and also show a message whenever I directionally control the Player or hit a collision.

Step 1 - Get my sprite2d example working

I am basically going to copy and paste the files in the sprite2d example PR into my source. I start with main.zig and the two shader files. Immediately, I must tackle the asset issue and pull over the assets too. I create a folder in {top level}/assets to house the assets first. I bring over the "sprites" folder from the sprite2d example. I copy the assets.zig" file over. I remove a bunch of the asset references in that file since I don't actually use a bunch of them. I end up initially keeping the "fonts" and "shaders" references because I suspect imgui needs those and I don't want to screw it up.

I better check my dependencies. The "zigimg" submodule library is missing so I add that. I am pretty sure the way I am referencing the dependencies in my "main.zig" file might be wrong but I will try to build and run to see what happens.

Sure enough, the build run tells me that references to zigimg and zmath are incorrect. I quickly realize that these two particular libraries need to be imported in a particular way based on what I have seen in the mach-example build.zig file. In my example, imgui has been updated to zgui. I am not 100% sure but I think zmath may have had some updates that require an update on how to include it.

I have some reading to do...

After reading the imgui updates, I realize I am a bit out of my depth. I could probably fiddle around with the build settings for quite a while but I have been down this road before. For example, I noticed that my local submodule of the libs/imgui repository is not the same as what I see in github. I decide to ask Stephen on Discord since I am obviously doing something wrong.

Stephen confirmed for me that I ran the right command to pull in the submodules I needed but something went wrong and instead of pulling the submodules from where I told it to (like git submodule add libs/imgui), it pulled from the main hexops repository. This was the source of my initial build issues.

After removing all of my submodules via a git rm --cached libs/{submodule here} command and then following the exact git add ... commands in the mach-examples .gitmodules file, the build didn't freak out about dependencies anymore.

It *did*, however, throw an error about not being able to find a shader for imgui. Of course, I didn't bring over the folder from mach-examples. Stupid mistake on my part but easy to fix. I do that.

Then I get an error about my usage of imgui.mach_backend.init in my main.zig file. Apparently, it expects 4 arguments and I am only passing 3. This must be an update to the imgui library since I used it last in sprite2d.

I am going to take this as a challenge. What changed? Fortunately, Zigs error output gives me something reasonable to track down. The "init" function now expects a "wgpu_device" parameter that my "main.zig" file is not passing. I go to the commits in the imgui repository to read the updates that have happened and see if I can figure out what I need. In the README, I notice the use of an init function and a "demo.gctx.device" variable which is probably what I'm after. I then realize I have incorrectly assumed what my error is. I am already passing the device. My "main.zig" file is shipping off "app.core.device()". I need more coffee, obviously. I've spent a few minutes chasing a bad lead. The real issue is the first parameter. The "init" function wants me to pass a reference to the application core. A simple addition of "&app.core" as the first parameter. Fixes that problem.

I have one more problem, it seems. I get another error about the imgui.mach_backend.newFrame function. It expects 0 arguments but has found 3. Just for fun, what if I remove all my current parameters and then comment out the "window_size" variable in my "main.zig" so Zig doesn't worry about an unused constant? It makes it farther in the build but then errors out because imgui cannot find a font file.

Of course. Assets again. I have to copy the fonts over from the mach-examples repo into my assets.

Holy crap! It works! Code here.

Step 2 - Get my Player struct merged

The first thing I notice is the use of Vec2 in the sprite2d example. This is probably better and more versatile than my x / y implementation. I will update my Player struct to use that for position. I also will have to update the "player_pos" variable in "main.zig" to go into the Player struct (rather, be taken over by it) and the "player_sprite_index" as well.

My vec2 definition is in a separate file called "vec2.zig". Ultimately, this is a bad way to organize this code. I could bundle this kind of stuff into a "helper" or "utility" file. For now, I'll just stick with what I have until I have enough stuff to start bundling together.

That does work but the movement is literally 1 pixel at a time. I would rather assume that I'm going to move one space at a time and I'll assume one space is 32 pixels. I'll just adjust my player move functions to add or subtract by 32.

That worked out well. Even though the player arrow does not point in the actual direction I am traveling, that's a problem I can sort out later. I just wanted the movement to be visible and they are.

Step 3 - Get my World struct merged

There actually isn't much to do here since we don't have the idea of a world in the current example anyways. I will just update the world size to use a vec2 and then import it into main.zig.

While I'm at it, the Position struct could just be a vec2 as well.<

Step 4 - Get my NPC struct merged

For each sprite in sprites.json that says "is_npc" = true, we create a new NPC initialization. I'll add an extra entry accordingly in the JSON file.

All I should have to do is add an "npc" variable, much like the "player" one, then look for an "is_npc" boolean. I can also apply a sprite index to it as well. If I do nothing else but that, it might even render, maybe in my players position if I just copy the player json data.

That's exactly what happens. If I move my player around with my keyboard, I can see the NPC rendered underneath it. Success.

Step 5 - Get the NPC rendered specifically

I actually got this for free in Step 4 but, just to be sure, I will initialize the npc world coordinates somewhere different so it's clearly rendered in a very distinct spot.

Success. Simply changing the x value to 128 definitely rendered my NPC in a unique spot.

Step 6 - Initialize Player, World & NPC data from JSON

I have basically already done everything I need for player and sprite. I could add some data for the world since I know I will use it later. I may as well do so now and make sure that works correctly at a very basic level.

I will just add a "world" entry in "sprites.json" with the size and maybe one or two blocked positions.

If I do this right, it means I can instantiate a world variable in "main.zig" and feed it the json data. I should be able to iterate the "blocked_positions" child in the json data and use the "add_blocked_position" method of my World struct.

I catch an error doing this. I forgot to update my "blocked_positions" struct member to use an array of Vec2 instead of an array of Position. Easy mistake to make so I will just update that.

Once I try to build and run it, I get an error that the zig json parser cannot parse my world "size" values into a vec2. This is causing me to scratch my head so I try to isolate the "size" value (remove the "blocked_positions") to be sure it's not anything else and that the compiler isn't lying to me. Sure enough, with only "size" left, I still get the error.

I quickly realize I am asking for a Vec2 when I should be asking for an array of f32, just like every other piece of JSON data. I bang my head on my desk and carry on.

Even though that was the change I needed, I get a weird memory issue when I run it. The window opens but the screen stays white and then an error appears. I once again repeat the same process (removing the "blocked_positions" entry) as I did when trying to debug the Vec2 error and, upon removing the entry, it works. So the array of arrays of f32 in my sprite.json, representing blocked positions, is causing an issue.

My initial JSONWorld struct had blocked_positions as [][]f32 in terms of definition. Is a nested array of arrays not acceptable?

I go read the zig json documentation to see if any of the tests there can give me a hint. I find no tests that contain arrays of arrays. Uh oh.

I am immediately embarassed when I go look back at my sprites.json file. I have been programming for years. I should know better. Yet, I still get bit by these little things because I don't thoroughly check my work. I left a trailing comma in my array of arrays. You can't leave trailing commas. Then I remember something in the json source code that had the word "trailing" in it and I go back. It's a json parser option!allow_trailing_data to the rescue! Or so I thought. Setting that as an option didn't actually help. Maybe it's for a different version of Zig? Anyways, I can just remember to not leave trailing commas for now.

Before I move on to NPC movement & collisions, I commit my working code.

Step 7 - Get NPC to move either randomly or on a predetermined path

The first thing I want to do is randomly generate an up/down/left/right so a simple random number generator should suffice. Just generate a random number between 0 and 3. I borrowed a random implementation and put it in a file. Then I imported into "main.zig".

My thought process was that I could simply generate a random value in the "main.zig" update function and then update the NPC accordingly. I would have to make sure I update the npc direction & position much like I do the player right now.

I write out the code and it mostly works. There's an error about a "try" inside the random number generator and a "try" must be run inside of a function. I did not do that so I wrap the code in a function and just import the function in my "main.zig". That works and logging the output of the random number generator proves it generates a random number every update / tick.

I write some hacky if / else statements so I can move the NPC using the NPC methods. That also executes but the NPC is not moving on screen.

Another stupid mistake that I have overlooked. Not only have I not imported the NPC class, I also have not initialized it with the npc variable nor updated any other npc related code appropriately. I do that.

To render the NPC properly, I probably need to replicate whatever I'm doing to the player sprite in the "main.zig" render function so it acts on the NPC as well. I create that code and run it.

That does the trick. Although, now that it's working and rendering, the random directional updates happen way too fast. I slow the process down by just setting an arbitrary global variable and waiting every 16 iterations as I increment it in the render function. Once it reaches the threshold I want, I reset it to 0. Ugly but it's just a simple way to ensure it works.

That looks and moves much better for me.

Time to do collisions!

Step 8 - Get simple blocks & collisions working

I first just want to see if I can get the player movement to respect the worlds blocked positions. That means iterating the worlds blocked positions and comparing it against the players current position. As an ugly and quick way to do this right now I can just do it after every player move. If there's a collision, then I move the player back in reverse.

As hacky as that code is, it works. For some reason, the blocked position is right where my player is initially rendered so, at first, it looks like it does not work. After moving away and then trying to move back, it works fine. It is a good sign.

Except for one thing. I notice that I have *two* blocked positions but only one is being respected. Plus it is at the initial spot where my player is rendered. This is probably some kind of calculation error. I decide I better fix this before I ask the NPC to respect this block too.

I decide it is time to give imgui some chops and show me the players world position. I have to give imgui a style to work with so I can expand the window to properly render the text. Then I update the "drawUI" function to ship out the content I need. I also convert my position float values to integers so I can read them easier. I also spit out all the blocked positions I come across when I update the players position. I do this in the console.

This leads me to understand that, in "world.zig", I have initialized an array of blocked positions with 128 entries. Each entry is initialized with a 0,0 position unless I override it when I load up my JSON data. This starts to make sense. 0,0 is definitely the starting point of my Player. If I watch my player position output in imgui I can reach the blocked positions in my JSON data and confirm it works, which it does. Ok, much better.

In order to fix this, I have to use std.ArrayList to initialize a chunk of dynamic memory for the blocked_positions member in the World struct. Then I have to pass the memory allocator from the app into the World initialization function. I will have to also update the add & remove block position methods to properly use append and whatever the removal is (probably a slice method of some kind). For now, adding is sufficient. I have broken the tests for "world" but I can come back to fix those later.

With this in place and fully working, I can finally get the NPC to respect the same world blocked positions so I update the code to do that. Some more hacky and repetitious code later, the NPC is also respecting the blocks. I also decide to fiddle with the blocked positions from [256, 256], [288, 256] to [64, 0], [128, 0] just to make sure I haven't messed anything up too wildly.

The final piece! Can I make sure the NPC and the Player do not walk all over each other? Should be easy, right? Just run a vice-versa comparison check with some more cringe code. Might as well go for it.


Notes and Future Work

  • When you use the spacebar to exit the app or you use the "x" at the top right there is a memory error that happens during the procedure. Most likely, this is due to imgui. I remember that this memory issue was only introduced in sprite2d when I added imgui so it is probably related. Stephen suggested the same in a Discord chat.
  • Would be nice not to have to use the ".0" in the json file and just have it automatically converted to float when importing.
  • When I updated how the player moves with the keyboard, I removed functionality for making sure the movement was smooth regardless of framerate. Down the road, I will have to deal with that again.
  • It would be nice to have trailing commas allowable in the JSON files I parse but this is a nice to have, not a need to have.
  • If I try to resize the window, the app crashes.
  • Write a better movement delay for the NPC, maybe based on the time and speed of the app, instead of the hacky hard-coded value you have now.
  • The structs at the top of "main.zig" could be moved to other files so "main.zig" is a bit leaner to read.
  • Fix the tests in "world.zig".
  • Figure out how you remove an element from an arrayList for "remove_block_position" and maybe update that function for "world.zig".

What's next?

I think I will have to clean up a lot of the code I currently have before I can move forward too seriously. However, I want to achieve at least one or two cool new things. It would be great to have a simple inventory system for my Player and a mechanism to buy or sell stuff from something. If I really want to reach for the stars in one session, I may include some basic stats for the player and NPC and allow an attack mechanism with health points. We shall see!