Friday, December 4, 2015

Unity3D At Home Project - Day 12 - Explosions and Stuff!


So first apologies that this next update took a little longer. First - Thanksgiving. And second, and this is more to the point - but ran into quite a few problems working through the next pieces, and it took longer than I would have liked to get everything working the way I wanted it to. And there are still issues, but it's time to post an update! We've got quite a bit to cover in this one, so let's get to it!

So when we last left off, we'd just set up some of our first protodata for mechs and weapons, and we'd built a nice effects package called WeaponEffect that allows us to group together a variety effects into a single entity that we can attach to just about anything. We had our weapons firing, but not doing anything. It was time to make them do damage, but once they do damage, then you have to deal with death, and once you deal with death, you have to deal with respawning, So that's what we tackled in segment.

Fun with Projectiles
I've set up the protodata on our weapons to support two kinds of hit-detection. The first, "raycast", will just do an instant raycast when the weapon is fired, to see if we hit something. We'll use this for beam weapons, and most ballistic weapons. The second, "projectile", will actually spawn a projectile and send it on it's way, and we'll let that determine what we hit. That method is a bit more involved, codewise, so I decided to tackle that first. It's not too hard to find simple projectile updating code. The effects package I downloaded had a nice projectile class in it, so I used that as a starting point. But our projectile has some special considerations. It has to be a network aware object, and it has to be spawned through the networking system. It needs a collection of effects associated with it's flight, and it needs a collection of effects associated with impact. So ours works like this.

Launch routine
When it's time to fire a projectile, the owning mech spawns a projectile, and then launches it. The launch routine establishes ownership of the projectile, so it can know who launched it, and what weapon it came from. We need this information for determining range, damage at impact, and who to attribute the damage to. On the clientside only, in OnStartClient, we initiate the projectile's flight effects. During update, on the server side only, the projectiles position is updated, and a check is done each update for collision. If a collision occurs, again on the serverside only, we inform the owning mech, and allow it to deliver the damage payload. At the same time, we initiate an rpc call to the client for the projectile, so it can play the impact effects at the point of impact. So as you can see, once again, whenever multiplayer is involved - everything gets more complicated, as we have to carefully establish the division of labor between things we want to do on the server, and things we want to do on the client.

Projectiles!


Death & Rebirth
So once we had projectiles firing, we needed to set up delivering of damage. Instead of just subtracting health, we created a "DeliverPayload" function, and it can do a variety of things. In our case, it applies damage, and it applies heat. We also then check against the mech's max health and max heat to to see if we're dead or not, or if we've overheated. I haven't even decided yet what "overheated" means, in terms of gameplay, but I've got a hook for it. For death though, it was time to figure out what that meant.

Side Note on Heat: I think heat management is a really important aspect to any giant fighting robot game. So I've put in plenty of hooks for it. Each weapon generates a certain amount of heat when it fires - so beam weapons generate more heat, ballistics not so much. Additionally, each mech has a heat dissipation rate, which is the amount of heat it naturally loses. When a weapon fires, it applies it's heat amount to the owning mech. Of the heat rises from zero, it starts a coroutine to dissipate heat. While the mech has heat, the coroutine reduces it by the proscribed rate each update. When heat gets back to zero, the coroutine stops. I wrote all this code and set all of this up for an effect I don't even have any gameplay simulation or visualization for yet. But that's what we do.

So it was time to think about death, and how we handle it. For the real game, I think I like the idea of when you die, you're out. Each match is a last man standing kind of thing, and when you die, the hulk of your mech stays on the battlefield, and your camera goes to following one of the mechs around that is still alive. But for practicality and testing, I needed to have a respawn. And, for some game modes, I could see respawn being desirable. So I needed to support it, and probably in a first-class citizen kind of way.  In order to support the notion of "gamemodes" though, I would need a game management class, and hell, even the notion of a game itself, which I currently didn't have. I wasn't quite ready to jump down that rabbithole just yet, so I just decided respawning, for the moment, is what we do.

Side Note on Death - Death is hard. No really - it's really really hard. Over the years, I can't tell you the quantity of bugs I've had to deal with centered around dead units. This is because it's a weird quasi state. There's some amount of time while you're dying, and then there's while you're dead, and the unit is still in gameplay, but it can't do all the things it normally can, and can't be affected by the things it normally can be. All I'm saying is - don't go into death thinking "Oh health == zero destroy unit done!".  That may be what they show you in the little game demos, but in reality it is never that simple.

That meant it was time to set up some game states. I have to tell you there was quite a bit of gnashing of teeth here between game states (dead, alive, dying), conditions (stunned, slowed, heated), and damage (heavily damaged, slightly damaged) and that sort of thing. All of these things are heavily interlinked, and the lines between what is one thing and what is another are hard to define. Get three programmers in a room and start talking about what is a condition versus a game state, and you'll be there for hours. I decided to punt on all of it for now, and establish some firm states, at least. They are Inactive, Spawning, Alive, Dying, Dead, Despawning.  This allowed me to establish some ground rules about when the player can control the mech, and when the mech can be affected by damage and what not.  Once I had all that sorted out, and code written to support all that, it was time to set up the effects for dying itself, and get those working. And those consisted of a death animation (which is a simple fall forward, provided by the Mech Constructor kit), some explosions, some sound effects, and a new fade out effect.

A Dying Mech
Problems, Solutions, and Workarounds
So getting all that set up was only half the work. In fact, probably less than half the work. Because once it was in, I then proceeded to spend the next week and a half fixing problems with all of the above. First the death animation wouldn't play reliably on clients. And when it did play it was 2 seconds too late. And then the fade effect wouldn't work. And then projectiles were wonky on remote clients (they still are in fact). The point is, I could go on for another three pages on the various problems I faced and the solutions I found, or in some cases, didn't find, but decided to work around. But I won't. What I will do, is give some short tips on how to deal with that kind of situation.

The most important thing is to isolate. Pick one of the many things that are going wrong, and focus just on that. Ignore everything else, and see if you can fix just that one thing. Once you have that working, then move on to the next single thing. Identify, Isolate, Execute. I know, it sounds cliche, but seriously, if you don't, you will be overwhelmed by the mass of shit going wrong in your game.

Finally - Combat!
Eventually, I soldiered on through the problems, and did in fact, finally get most things working to a satisfactory degree. Mechs can fire weapons. The weapons generate projectiles, do damage, and do heat. Mechs can die. When they die, they explode, sound plays, and then fall forward, and fade out. After the mech fades out, the player is respawned in a new mech, at one of the spawn locations. The circle is complete. Finally I was able to hand my wife a game controller, which she relunctantly took, and we drove the mechs around firing indescrimintely at each other, blowing each other up, and respawning. You can see the results below.


Aaaaand we're done.  Or.. are we.. 
Well, technically, I'm done. I have actually at this point, met the full "specs" of my original design - which was to get to a point where I can drive a giant robot around and shoot other players in a giant robot. And we can now do that. But as you can see, we're no where near close to a real game. We don't even have a UI. I feel pretty sure I'm going to carry on for at least a bit further, but there's a huge amount of work still to be done in turning this into a real game.  So if you - yes you - actually have any thoughts on whether or not you'd like to actually see and play a finished game created from these humble beginnings, I'd be happy to hear them!

Useful Links
No real useful links this time. I spent all my time rummaging through the unity multiplayer docs, which I've linked multiple times before. At this point in the game, we're mostly out of the "tutorial" stage, and soldering on in uncharted waters of our own.

The Unity3D At Home Project
Here is the main page to the entire project, if you'd like to catch up on previous posts and see this from the beginning.