SoC:001 - Weapon Development
February 9th, 2018, 3:32pm
I wanted to start doing more writing on my blog in an effort to keep myself writing, I suppose. Specifically on the game I've been talking about making for nearly, oh, two and a half years now. Yeesh. The good news is I'm actually working on it pretty consistently lately, so maybe writing these posts will help keep me excited to work on it.
Weapons are pretty critical in a game focused on piloting a giant car with legs that blows stuff up. I've messed with implementing weapons in the past, so fortunately jumping into this particular aspect of the project wasn't too terrifying. Still, I really want to make sure that the system I build is flexible enough to customize into lots and lots of weapons without being overly complicated. To start, I doodled up a general idea of what I thought the system would entail in my notebook.
I should probably say first that this was NOT the first thing I did when coming up with how to handle weapons. The first thing was
I've played a lot of shooters, especially older ones. If you too play (or make) them, you probably know weapons fall into one of two types: hitscan and projectiles. Every ranged weapon in every FPS (or any game with ranged weapons, really) falls into one of these two categories.
Hitscan weapons are the "faker" of the two. Technically, in the real world, all ranged weaponry send out some form of projectiles, be it bullets, rockets, potatoes, what have you. All of these projectiles move very fast, but some are so incredibly speedy that simulating them in a physics system for a game is very impractical. So, as with all things impractical in video games, we fake it. More specifically, we simulate these very fast projectiles with raycasts, aka hitscans. What this does is draw a line from point A to point B (in our case, from the barrel of the weapon to the point in space where our on-screen crosshairs are pointing), and the first item found intersecting that line is what got hit. This works great for things like rifles, machine guns, shotguns, pistols, rail guns, anything that fires bullets or projectiles that, while technically do take time to get from point A to B, seemingly travel instantaneously.
Everything else that doesn't fit into the hitscan approach follows the more traditional projectile method. Stuff like rocket launchers, grenades, laser blasters (I'm looking at you, MechWarrior 2), or anything else you can think of where the weapon produces an object that travels at a rate visible to the naked eye. In games, these weapons typically spawn items at the end of the barrel that travel towards the point at which the on-screen crosshairs were pointing at the time of firing.
The whole point of splitting up weapons into these two categories is so that different weapons can then be designed using varied parameters instead of dedicated code. That is, all hitscan weapons operate in the same way, so by changing a few values like rate of fire and damage per shot you can have a wide variety of weapons that all use the exact same code. The same goes for projectile weapons - why write different code for rocket launchers, grenade launchers, monster launchers, etc. if they all basically do the same thing?
So, cool, the weapon types are abstracted. Done, right? Nope. Because some other things need to get split up too. And this is about where I started exploring new territory in my weapon coding adventure. Let's start with fire modes.
In my previous weapon control code projects, I usually just wrote two separate scripts, one for handling hitscan weapons and one for projectile weapons, and called it a day. But one thing that always bothered me about this was there was still a fair amount of rewritten code on both that did essentially the same thing. One of those things was handling firing modes. That is, how many rounds it fired while the fire button was held down, how many shots per round, how rapidly it fired, etc. At the time I just accepted this discomfort and moved on, but I'm all the wiser now and managed to split this out into it's own code through the magic of C# interfaces.
Interfaces are, essentially, templates for a type of object that you fill out later. The benefit of using them is any object that you create that implements an interface is guaranteed to have the same methods as another. For example, you could have an interface ICrushable that says you have to have a method Crush applied to any object implementing it. You apply it to a Can object and a Paper object, both of which have their own implementation of Crush. Later, you can make get all the objects you have with the ICrushable type and Crush them without worrying about the differences between Can and Paper.
In the case of Fire Modes, I put together a simple IFireMode interface that contains one method signature, FireCoroutine. Then I made three different Fire Modes (SingleFire, BurstFire, and RapidFire) that all do different things inside FireCoroutine. Finally, when WeaponController (my single script that connects all the dots between the different weapon system scripts) starts the firing coroutine it doesn't care how it's firing, just that it starts FireCoroutine. This also means if I come up with some new kind of fire mode, I can just write a new class that implements IFireMode and boom: new kind of fire mode.
This would be a good time to mention that I actually created the hitscan and projectile weapon types from another interface, IWeaponType, with the method Fire(). This gives me the same level of flexibility I get with IFireMode, so I could easily integrate new Weapon Types into the system I currently have.
Remember that bit earlier where I talked about the ray or projectile going from point A to point B? That's trickier than it sounds, and where targeting systems come in. Point A, the barrel of the weapon, is easy enough to determine. In my current implementation, it's a transform that's directly added to the WeaponController component and passed on to the relevant scripts. Point B is the part that's tricky. Just what exactly do you want to hit with your weapon? Is it what they player's looking at? A locked target somewhere else on screen? Maybe a specific point on the map?
The whole point of targeting systems is to determine just that and provide that "target" to the weapon. ITargetingSystem has two methods, one of which is AcquireTarget(). It returns a Vector3 that serves as a target for a weapon to fire at. And right now, I've only got one implementation, BasicTargetingSystem, that just provides the camera with whatever object lies in the center of the on-screen crosshairs. This is a pretty dumb targeting system, but also an essential one. I'd like to add some more fun ones though, like a lock-on system or something with multiple targets (a death blossom targeting system, maybe).
Putting it all Together
Whew, that was a lot of info dumping. If you're still following along, you might be wondering just how all these things get tied together and used. The parts I've talked about all get plugged into a main control script, WeaponController, and that gets defined by a data object called a WeaponDefinition.
Surprisingly, WeaponController is a pretty straightforward script. The hardest thing it has to do is configure itself based on a data object that's provided to it describing what kind of weapon it is and how it should behave. After that, there's a couple methods called via button presses that start or stop it from shooting. By separating the components I talked about earlier, I was able to keep this bit of code very simple and very small.
This bit's also pretty simple. It's actually a scriptable object, a sort of data formatting template that Unity lets you edit from within the editor. This particular implementation let's me specify a weapon type, fire mode, targeting system, amount of damage, firing rate, what prefab to use (if it's a projectile weapon), it's range, etc. WeaponController then takes this data and uses it in it's configuration to create a unique weapon for use in the game. Pretty neat stuff.
Even after writing all that, there's still a lot I have left to do with weapons. I'm not entirely happy with how Basic Targeting works in it's current state, as it's unrealistic to target an object that's a foot away from the mech and have the projectile or ray shoot directly at it. Also, I need to actually create a bunch of different weapon definitions and start playing around with them to find what's fun and what isn't. But for right now, it's a solid base to jump from and start working on something else. Right now I'm looking at either fixing up the mech movement system or developing some enemy AI so there's stuff to fight. Either way, I need to do both so they'll wind up in the game at some point.
Oh hey, you're still here. FUN FACT: there's a prize waiting for you at the end of this post!
Something I've been meaning to do is start testing builds of the game weekly, and now's as good a time as any to do just that. So, since you've made it all the way down here, I want you to be the first to know how to get in on the action on itch.io. All you need to do is click the button below; that'll take you to the store page for the game. I'm not publicly listing this page, so keep it a secret! Or don't, that's cool too. And naturally, tell me your thoughts on it. Just keep in mind that things are still in (very) early development, so it's less a game and more a testing bed.
How to "Play"
The game controls similarly to a standard FPS, but with a couple minor differences:
- W/S - Walk forward/backward
- A/D - Turn left/right (Yep, no strafing on these streets)
- Mouse Move - Rotate the cockpit around (The green circle on-screen indicates the center orientation)
- Left Click - Fire the weapon
- ~ - Open the console (What's it do? No one knows...)
Alright, gonna close this one up here. As always, you can hit me up on Twitter with your comments/questions/thoughts/opinions on Carbon (or anything else, really). Hope to catch you again next week!