Yoo this looks lovely!!
Excellent work on the light/fog and also on the sweet sweet camera motion. The Playdead inspiration is clear, in a good way
RE: the blinking lights, I'd do it in a shader for cleanliness and performance reasons, but I really like writing shaders and tend to be overanxious about perf, haha. I'd write a shader that uses vertex color and blinks them on/off based on _Time, with the intention that they all be batched together with one material.
Thank you! I want
long draw distances and a lot of realtime lights so performance is a real consideration. I’m currently a bit intimidated by shaders, so it’s good to hear that there are people that enjoy writing them… Hopefully I find that I'm one of those people because they seem super useful.
I love that atmosphere here on display, looks good!
That protagonist robot is quite cute, are you planning to go with that? The Brave Little Toaster :D
That concept art looks quite dark, what's the tone here? Looking at it, I'm thinking Amnesia, or The MAchine for Pigs etc. You know, I expect dark violence and horror... IS that the goal? Little NIghtmares kinda vibe?
Thanks! Glad you think it's cute! I am planning on going with that and why will be a bit more obvious when I manage to get another core feature implemented. I'm going to keep that secret for now, though.
Brave Little Toaster might be a decent comparison, I recall that movie being fairly dark...
My concept art might be selling 'horror' a little too hard, but we'll see how things develop. The story I have in mind is dark, but not really in an overtly violent or horrific way. I'm targeting a tone like Hollow Knight or Metroid. The little bot you control is
not helpless.
This looks really nice. Im looking forward to seeing some more progress
An even more in depth tech talk about how the 2.5D system works would be very interesting!
Thanks! It's actually a good time to talk about it in more detail - I'm currently refactoring my character controller to make it more extensible, which entails remembering and/or re-learning how it all works anyway...
So here we go:
This is a technical post2D Control Along a Bezier Curve“2D control along a bezier curve” is the most accurate short phrase I’m able to come up with to describe the implementation. Basically, I wrote a 2D character controller and I ‘spin it’ based on the tangent of the closest point along the curve. Less basically, the whole process looks like this:
NOTE: Unity uses the Y axis as the up axisStep 1: Make a Bezier CurveI'm using the excellent/free/MIT-licensed
BGCurve for creating splines. If you want more info on splines themselves, there’s
this excellent tutorial from Catlike Coding. That site is worth a look even if you don’t care about splines, it’s a treasure trove of great Unity information - mostly how to accomplish things that need some amount of fairly intimidating math.
So I’m technically using Splines (3D bezier curves) but
most of the time I just throw away the Y axis information.
Step 2: Make 2D Character ControllerMy character controller setup is getting more complicated for modularity reasons, and I may go into that at some point, but the gist of it is that
most of the character controller ‘thinks’ of the world in 2D. I’m using Vector3.forward as ‘right’, Vector3.back as ‘left’ and Vector3.up/down for up/down respectively, but I could very well be using a Vector2 instead.
Then I store this vector as the character’s “Goal Position”, though I'm just calling it 'velocity' in the script.
This is
before checking collision, too. It’s basically just “This character is trying to move forward or jump or take this action” at this point.
Step 3: Determine what direction 'forward' isSo then we need to determine how to rotate that “Goal Position” vector so that ‘forward’ is the closest point along the bezier curve. Note that I don’t mean the nearest ‘control point’. BGCurve allows you to determine the granularity of a curve, and how many points are plotted between each control point. I think I’m using whatever the default is.
BGCurve does all the heavy lifting here, and I went ahead and posted a
GitHub gist of the class I use to streamline this. Really, this class should just extend BGCurve and do it all that way, but I lost BGCurve's GUI when I did that. I'm sure that's an easy fix but I didn't want to mess with it at the time.
Step 4: Course CorrectionI wouldn't need this step if I had some way of ensuring the character was always precisely on the path - like if our 'forward' calculation had a
really high resolution and we always placed characters perfectly on the path. This isn't really feasible, so I just apply some course correction instead.
You'll notice if you looked at the gist that I'm saving the point on the curve while calculating 'forward'. So I make a vector between that and the character's position, throw away the 'Y' axis, and Lerp between that and 'forward' based on how far off course (the distance to that point) the character is.
See this image, where the white line is little bot's forward and backward directions, and the red line is the correction vector.
Course correction at work:
This would be simpler if I chose to walk RIGHT instead of LEFT... That purple line is actually the OPPOSITE of the correction vector, due to moving left being negative and some messy internals that will be fixed in the refactor.
You might notice I'm actually doing all of these calculations based on positions
on the ground. This is so that paths can cross over themselves without your character latching on to a path above them when they jump:
(The upper path is much closer to the character than it looks in this gif...)
I actually do this by simply replacing the character's Y position with the Y position they had last time they were grounded in all relevant calculations.
Step 5: Rotate the CharacterNow that I know which way forward is, I
could do some fancy vector math to determine which way to march... or I could just rotate the character to look in that direction, then manipulate their movement in local space! Both would work, but the second way requires less mental overhead, so that's what I do.
Step 6: Check CollisionsI check environment collisions in local 'pseudo-2D' space using raycasting (from the 3D physics engine) instead of Colliders for those sweet fake physics that act like you expect a 2D platformer to act. I'm doing this in a fairly standard way for a 2D character, I think. I based my implementation on a tutorial series by Sebastian Lague. (EDIT: I had it linked but it embedded the video instead and I don't want that so you'll have to search, sorry!) I might have gotten my jump implementation from somewhere else though - it lets me plug in a "Time To Jump Apex" and a "Jump Height" and magically calculates the needed velocity. I'm adding extra gravity on descent, too, that's a trick stolen from Mario.
You can see the collision raycasts visualized here, where cyan means no collision and red means a collision. I'm also using red for the line down to the floor for some reason, and I don't know what that cyan line is supposed to be showing
I'm running a lot of raycasts for the player character. I
think that this is due to slopes, but maybe I could get away with less.
Then, again in fairly standard 2D character controller style, anywhere where there is a collision detected I move the "Goal Position" outside of the colliding object.
Step 6: Place The CharacterNow that we know where the character should be this frame, we finally apply the position to the character and we're done.
Other ConsiderationsI'm calculating slopes as if they were 2D slopes so if you have questions about that check out Sebastion Lague's tutorial I posted above. I had a bit of trouble rotating the character to affix to the slope, and I think I just brute forced that by trying a bunch of random Quaternion math until I stumbled into something that worked.
To handle path changing, I simply place Colliders (with 'isTrigger' turned on) on the same object as the curve, and those tell any object that can change curves to do so upon entering the collider.
I'm moving around a lot of this code, but the refactor will follow basically these same steps with a new modular pattern where I can give and take various behaviors.
As just a piece of general advice, whenever you're calculating stuff in 3D - DRAW IT!! I always set up some 'logging' booleans so I can turn my debugging drawing on and off easily, and leave it in even when I think things are working so if I run into a weird edge case I can turn it back on.
End of technical post
Whoa that took me nearly 3 hours to write out and edit instead of the 1 I had planned. Time to get back to the refactor before I've lost my whole day to forum posts!
(EDIT: Then I spent another 15 minutes making random edits! What am I doing!)
I hope the tech update was useful. I may still release it all as an asset down the line, but it depends on how clean the refactor is and how willing I am to write documentation...