With this update comes combat! It's currently missing proper lag compensation, and the player models are super duper placeholders yoinked from an older project, but I am one guy give me a break.
The goal for this update was just the combat, but before I could even begin with that I had to transition blocks from being instantly destroyed to taking damage gradually, as touched upon with the previous update. For context, when I first created the original world/block modification system, blocks where just an ID mapped to a position which corresponded to the actual data from a big list of block data. The issue with this was that since you're essentially referring to a table of persistent blocks, individual blocks couldn't themselves hold any data. To fix this, I had to create a 'BlockInstanceData' data structure, which to begin with was just a wrapper for the block ID, and nothing else. Still, a good start! At that point I essentially just did a find and replace converting the number ID to the BlockInstanceData. Hooray! From there, I just needed to add a value corresponding to the health of the block ranging from 0-1 and shove that into the instance data. That all gets serialized properly and sent over the network as the previous system would have sent a block ID, great. Then, I hooked into the base 'block update' method, which is called on a blocks when it, or any block directly neighbouring it, is modified. If the block update method detects its health is at zero, it promptly destroys itself.
So with the backend sorted, I needed to implement some visuals. I did so by expanding block data from having just one material, to several that change depending on the blocks health. This did require some slight adjustments to the chunk mesh generation, to allow for the increased number of materials, and may seem like overkill. After all, why go through the trouble of all that when you can just apply an identical breaking overlay over all blocks being damaged? The benefit of this system is twofold: first, it allows for blocks to remain damaged persistently without the need for rendering an individual overlay on every block; second, I (or more likely an actual talented artist if any want to help out) can create individual breaking textures for different block types. After all, stone breaking looks very different to dirt breaking.
From there, it was finally time to implement the item system.
Before anything else, I just needed the most basic functionality of bringing an item into and out of existence based on what the player selects. The initial test for this was simple: a cube object, and a sphere object. While this may seem trivial - just enable and disable the object depending on what's active - in truth the required solution was a little more complex. The reason for this is that item behaviour has to also be enabled/disabled with the item. Due to the design of Unity's Netcode system, which requires every 'NetworkBehaviour' to be attached to a Network Object, that means that the item has to be its own object independently spawned over the network. The solution to this was a small pain to implement: instead of just having a reference to some items to enable/disable, the ItemManager has a reference to the original prefab (template) for each item, which it then at runtime spawns and attaches to the player. From there, it is actually just as simple as syncing what item is selected and enabling/disabling the appropriate item. Simple!
Next I had to implement the block items. Prior to now, there was one 'BlockSpawner' object attached to the player that would destroy the block at the crosshair when you left click, and place the selected block when you right click. Before adding any sort of weaponry, I had to transition from this system to one where each selected block was its own item object responsible for placing itself and damaging blocks. I created an item script to place/damage blocks (with a cooldown so it is no longer instant), while also ensuring that all the behaviour is predicted on the client so it feels snappy and syncs across the network nicely. That one sentence took 90% of the time of this update. I wish I was joking.
Up next: the shotgun. The first thing I did for this was simply to create the state system for the shotgun: no animations, no damageing, no visual feedback at all, just what was effectively a series of checkboxes saying 'I am firing' or 'I am reloading'. My thinking was that if I had this down 100%, everything else would come much more smoothly - I learned this the hard way through an old now-cancelled FPS game. Luckily, I was right. Once I had that done, the rest came relatively easily. For instance, all I had to do to implement animations and ensure that they synced across the network was to hook an animation control into those afformentioned checkboxes. Surpsingly, it actually worked quite well
Speaking of animations, there is an animation system. I realised pretty quickly I couldn't really test the animation system with nothing to animate, so I simply took an old player model I used in a game once 2 years ago that was already fully animated and used that! As mentioned, actually syncing the animation state wasn't too difficult with the item system as it was. The hard part with animations was getting the player to interact with their items. The reason for this was because, as established, the items aren't attached to the player but rather instantiated later on, which meant I didn't have the option to animate with them. Furthermore, this would have caused issues if I ever wanted to re-use the same animation for a different item, for instance in the case block items which have the same animation set but use different blocks. To solve this, I used what I call 'Animator Proxies'. An animator proxy is essentially a blank object attached to the player with a key, such as "WeaponPoint0", which can be freely animated. Each item has on it an 'Animator Proxy Reciever', which is constantly listening for an animator proxy with a matching key. The proxy broadcasts its position, rotation, and/or scale (depending on its configuration) to all connected recievers every frame, and the reciever with the appropriate key will transform to match the proxy. This can be used for something really simple, like with block items which just have one attached proxy to snap directly to the player's hand, or slightly more complex, like the shotgun. The shotgun has two proxies for its animation to function: one to attach it to the player's hand, and another to open the shotgun's break-action.
With the animation system complete, time to actually give the shotgun some functionality. To do so, I had to first create a health management system for the player. The first part of this was of course the health value itself, which was made pretty trivial by Unity's NetworkVariable system - it's just a float value that syncs automatically, easy! From there, I had to implement death and respawning, which is slightly more of a pain. Luckily I had already thought ahead and ensured that the player 'avatar' (the bit that you actually control) and player 'manager' (an etherial empty object storing persistent data for the client) are kept secret. The benefit of this is that I don't need to worry about doing any 'fake' death, by disabling the player and then re-enabling them at the respawn point, which is a pain to implement and prone to breakage. Instead, all I had to do is destroy the player's avatar, and then allow them to be respawned by the pre-existing spawn management system.
The spawn management works by having each spawn point have a series of 'requirements'. Until now, the only requirement was that the initial spawn chunks have been generated by the server to prevent the player spawning and falling into the void if not all chunks had yet been generated. Because of this, there was no 'respawn' UI, the player manager just automatically requested for an avatar to be spawned instantly and it would be spawned. The first step in revising this to be more user-friendly was having a big header telling the player what the current spawn state is: either pending, or ready. Then, I made it so that instead of the player manager automatically requesting for the player to be spawned ASAP, the player can themselves press a button to request an avatar be spawned. In future, this will allow me to place a class select screen at this point so the player can choose their class before then deciding to respawn. For now, it essentially just operates as a timeout. With all that done, all I had to do was add one new spawn requirement for respawn time. The system as it is now is pretty simple, every player just has a respawn time, which resets to 5 seconds when they die, that is constantly ticking down and must be at zero for them to be allowed to respawn. I then created a UI message to show the player how long is left on the respawn timer. And that was it!
That was a very long devlog, and a lot of words. Not my proudest work, but this update was a big one! It does of course represent a big step. This means that all the core gameplay systems are in place in terms of the player itself. From here it's just a question of implementing the game rules themselves in terms of teams, rounds, gold, and then it's just Adding Stuff - more weapons, the three classes - and that is it.
peterkirkcaldy@gmail.com