I. VERY NEARLY FLAT
It was only about a month ago that I started fooling around with this prototype for a 2D idea in Unreal 4, but it’s summer 2017 and time has become weirdly elastic, a state of affairs not improved by the glaring red sun that’s loomed over the PNW behind the smoke of half a dozen burning forests for much of the season. It’s been an odd time to be trying to make progress on a self-motivated game project, especially with all the sundry financial and existential anxieties that stem from that inevitable gap between actual paid game development contracts. It’s a drifter’s life, I tells ya!
All the same, one must smoke the old stogies one can find, as it were, so I’ve waded in and started to build the thing out. The basic principles here are these:
- A 2D game, whether built in actual 2D or not (more on that in a bit), meaning all game action takes place on a single plane, there is no movement “in” or “out” (regardless of how that’s actually implemented, again, more later).
- Aimed to be played with a controller or equivalent; two rotary thumbstick axes and a bunch of buttons.
- Featuring a player character who can jump between platforms (that’s the easy part)
- and can pick up and throw objects, specifically boxes (this is not so easy)
There’s a lot more to the idea, but that should provide enough context so that I can explain how I tried to make this basic stuff happen. The first thing to note is that since I’m using UE4, I’m not actually making a 2D game, despite the charmingly named “Paper 2D” ‘system’, which is actually just a few useful classes for sprites and backgrounds and such. You can import anything you draw as a texture, which you can then convert to a “Paper 2D Sprite Actor” and slap onscreen wherever you like, moving it in response to player input or your own scripts. However, if you want that thing to collide with another thing, you’re going to be moving into the world of 3D.
OK, that’s not 100% true, you can make use of Paper 2D’s 2D collision system, which will allow you to keep things in real, genuine 2D. Here’s what Epic themselves have to say when you mouse over this option:
As much as I love beta testing, if I’m trying to make a game I generally steer clear of workflows that the engine devs have labelled “EXPERIMENTAL” in all caps. I’m aware that’s not a philosophy that I adhere to in any other aspect of life, but let’s set that aside. Some forum-searching on the topic led me to several threads concluding in the assertion that the best thing, for now, is to stick to the classic Capsule and Box colliders, provided you want your collision to actually, you know, work. On balance, while working in pseudo-2D had its own headaches and overhead, it also had the advantage of being familiar, which added speed. With an orthographic camera, the player won’t know the difference, unless of course the physics engine freaks out and flips the player or the whole scene by 90 or 180 degrees, which only seems to happen once or twice every few minutes.
II. LOOK WHERE YOU’RE THROWING
The controller is where things got interesting on the design side. The concept I had was: player moves with the left stick, picks up boxes with a button, then aims with the right stick and uses that same button to throw. Simple enough, eh? I made some floats to hold the right stick’s position, and populated them via event handlers in the controller blueprint:
I felt the player needed some kind of visual indication of where the throw was “pointing” in order to play the game. More pertinently, I needed such an indicator myself so that I could figure out whether I was implementing the aim correctly. I have several ideas for how this could eventually look, and it’s likely that I’ll use the player’s arm positions and/or animations to communicate this, but the initial kludge was to just put a big circle around the player with an arrow pointing in the direction of the throw. Now all I had to do was figure out how to make the right thumbstick control the rotation of that arrow.
After entirely too much trial and error, I settled on this method, executed in the Character blueprint every tick:
- Use an Atan2 math function on the two floats representing the current x and y position of the right thumbstick, resulting in a third float representing the arc tangent.
- Invert that float (multiply by -1). I don’t remember why exactly this was necessary, but… it was.
- Use the Make Rotator blueprint node to generate a new Rotator variable, with that inverted arc tangent as the Pitch, leaving the Roll and Yaw set to zero.
- Set the Relative Rotation of the circle sprite (a component that lives on the player character) using the Rotator you just created.
OK, so that’s half the problem, this makes the arrow point in the direction the player is “looking”. So how do we make a thrown box travel in that direction?
My understanding of OOP dogma is imperfect and incomplete, but I do like to follow in the spirit of concepts like encapsulation as best I can, so in this case I put all the logic for throwing the box in the Event Graph of the blueprint for the box prop itself. So technically, the player doesn’t throw the box… the Player Controller says to the Player Character “I’m pressing the button”, which in turn causes the Player Character to say “Hey, any box that I may happen to be holding? Please throw yourself now”, and then the box itself goes and does it. This is not intuitively the way I understand the world (or it wasn’t until I started doing software development), but I’m trying to adhere to the principle of letting each object be responsible for itself and nothing else, communicating only in messages to the effect of “it’s time to do that thing that only you know how to do”. Only time will tell if this approach turns out to have been worth the trouble.
For the throw function, once again we’re starting with the two floats generated by the Controller to represent right thumbstick x and y positions. After boosting those values (about which more later), we use a Make Vector node, feeding in the two thumbstick values as x and z, and leaving the y at zero (since we’re using 3d physics, we need 3d vectors, but with a zeroed-out depth plane). That vector then gets fed into an Add Impulse node targeting the Render Component of the box, which is the component version of a Paper 2D Sprite, where the physics for the box are handled.
This is great and sends the box in the correct direction, but under no power whatsoever, as those thumbstick values are between zero and one. Clearly we need to boost those values before making the impulse vector out of them, but by how much? A lot of factors went into this, and I could write about juggling the mass and gravity values while trying to keep things feeling realistic, or at least consistent, but to keep from endlessly digressing I want to move on to what I found most interesting about this: creating the “dynamic power system” that I used for both the throw and the jump. The inspiration for the “feel” I was after came from hours spent playing one of the best platformers of the past several years, Hollow Knight.
Setting aside Hollow Knight‘s fantastic art, story, mood, sound and other formidable achievements, it’s a game that’s very serious about the foundations and fundamentals of 2D platforming. The jump feels great, and while I haven’t tuned my own to be anywhere near as satisfying, I was definitely using their jump as a benchmark. The longer you hold down the jump button, the higher and farther the player character jumps, up to a maximum. It’s as old as Mario, and has been used in a billion games since, but when built with care it gives the player a sense of power, control and precision that just feels correct in some undefinable way.
My version of this uses a built-in node called Add Jump Force. I string a wire from the On Tick event, and every tick I query the Controller to see if the button is being held down. When the button is released, another stock function, Jump, does the actual work. These are among the many advantages of going Quasi-2D: this stuff is inherited from a Character Movement component, which requires the stock Capsule Collider for its physics, assuming you don’t want to roll all your own movement logic. That could be worth doing once Unreal’s 2D support is more robust, but for now it seems like the only sane path is using the 3D tools provided (or, of course, picking a more 2D-focused engine, which is always an option).
I ran essentially the same play for the throw, with the addition of manually capping the number corresponding to how long the button has been held down: once you’re at 100% “throw power”, it stops incrementing. I also used that same value to change the color of the circle sprite, so that it becomes more red as you hold down the button, giving the player some feedback as to how much they have “charged” the throw:
One interesting thing about this: I was doing some related online research only yesterday which led me to discover that the solution I settled on is almost exactly the inverse of the agreed-upon standard for doing this. As made clear in this article from the Sonic Physics Guide (yes, it’s real, and it’s spectacular), the traditional method is to execute the jump at max power as soon as the button is pressed, then manually cut the player’s upward velocity when the button is released. For all I know the Jump function is doing something like this under the hood, but the throw is definitely not, and it may be worth re-visiting this later to see if there are any advantages to not implementing it backwards. At the very lest I’ll keep the concept filed away in case any weird edge case bugs crop up around the current jump and throw mechanics.
III. DUDS AND SNAPBACKS
Designing specifically for analog thumbsticks resulted in some unique challenges. For instance, I had an ongoing problem where the aiming circle would “snap back” to pointing straight up whenever I released the right thumbstick. Since the function was reading values from the hardware every tick and rotating to suit, as those values approached zero (thumbstick at rest in the center), the Rotator variable would also approach zero and point the thing straight up. I needed a way to keep the aim where the player last *intentionally* placed it. I went down all sorts of rabbit holes involving gamepad dead zones and value clamp functions, but the solution ended up being very simple: Only change the values of the Vector, Rotator, etc. if either of the right thumbstick input floats is equal to one. This means we only care about that stick when it’s at the “rim” of it’s range of motion. Once the player is no longer pushing it as far as possible from the center, in any direction, one of those floats will fall below one, and at that point we just nope out of the function that updates the values.
Similarly, some number juggling was required during the throw, for cases where the player is aiming straight up. For some reason I have things set up so that pointing up produces a y value of zero; the throw function multiplies that by throw power and some arbitrary boosting force, resulting in… still zero, of course. I solved this simply by listening for that value to hit zero, and then flipping it to one. This hasn’t had any unintended consequences so far, which is about the best one can hope for these days. Your commit hasn’t broken anything yet? Ship that puppy.
All this just to implement some basic platforming gameplay that I haven’t yet verified is any fun at all… but you have to start somewhere, and I now know enough to build out a level and test some theories. The idea is that level traversal should involve some puzzles that can be solved by throwing, stacking, and jumping on boxes. The variable throw power mechanic was crucial to pushing this forward, as otherwise throwing boxes was an exercise in frustration, and stacking them was out of the question. The next step, of finishing a basic level, will tell me whether I have enough of a foundation to build on, or whether I need to go back to square one and re-think some things.
Thankfully, it looks like a return to paid game dev work might be looming in the near future, so I won’t be betting on this game’s success with my livelihood, at least not today. The downside is that paid work tends to make solo projects float in the direction of the back burner, but at this point that seems like a small price to pay. Even a drifter needs a tin roof overhead once in a while.
- Lighting sprites in Unreal
- NPCs, UI elements, and other distractions
- A new series on maps? Quite possibly!