Our game has flags to determine what's happened before, and what can/can't happen now. Flags are basically booleans that get turned on and off when certain events occur, like getting a frog. As an example, when you get Frog#1, a flag called "gotFrogOne" will be set to true. Later in the game, there might be a portrait. If gotFrogOne is false, the porttrait would be empty, but if it's true, the portrait would have a beautiful picture of the frog in it. I don't think this exact example will be implemented, but that's pretty much all flags are for. A game like Undertale is constantly updating and checking flags to improve the player experience, as tracking player choices can help give these choices a lot more meaning.
My improvement to the flag system we have was simply moving it into a singleton object that's accessible in all scenes, with the data persisting throughout.
I added a singleton object for audio as well. Any object will be able to call the AudioManager's functions, and the sounds will be played from this object. For example, when the player jumps, they can call "AudioManager.PlaySoundPlayerJump()" and it will play the sound for us. As this is a singleton, it exists in every scene, can't be deleted, and the data inside of it is consistent throughout the entire game. Additionally, if we wanted to replace the Player Jump sound effect, we would only have to do that one time, and it'd update throughout every scene.
Freezing Objects in Cutscenes
This is a really interesting hurdle with a really fun solution. This could probably be a blog post all on its own, and I might actually do that at some point. The short of it is that we don't know exactly what will be frozen in cutscenes, and it'd be annoying if the object that calls "freezeAllObjects" had to check every possible object and script in the scene for criteria from a big list of possibilities. Is this object the player? Is it a projectile? Is it an enemy? Is it an NPC in mid-air? On the ground? etc. etc. etc. The obvious solution here is to give all objects that can be frozen a common trait. Unity has tags, but each object only has one tag, and some objects already have other tags. Instead, I created a new script called "FreezableObject" that can be attached to every single object. Now, when we call "freezeAllObjects," we can just call "Object.FindObjectsOfType<FreezableObject>()" and we'll have access to everything that can be frozen. This makes dynamically creating things that can be frozen a synch (imagine an enemy creating a bunch of projectiles).
This magical script is pretty abstract right now, and uses a feature I really like about C#. This is the delegate keyword, which is, in my head, the equivalent to function pointers in C++. The way that delegate works is that we can have a publically accessible function, called Freeze(), and that's all we have to call from the outside. On the inside of the script, we can check to see what Freeze() needs to do, and then point it to an existing function that will accomplish that task.
When this code runs, it looks to see if we're the player (GetComponent<PlayerControls>()). If we are, it assigns this freezeObj function call to the PlayerFreeze function below (not pictured). If we're not the player, it checks to see if we're a projectile, and assigns the functions accordingly.
This now just needs a check for the other types of objects that can be frozen during a cutscene, and it's easy to update with that functionality as we move forward.
This week I took some time to go through feedback and make some small tweaks. I want to constantly iterate on our movement and combat until we reach the point where the gameplay feels so natural to players that we never get comments about it. Although some praise here and there would be totally cool, too.
I didn't spend too much time on the movement this week, but I did address a few issues. First of all, our jumps were a bit too floaty, and the inputs were occasionally being eaten. I changed the jump arc to make us fall faster, but our initial climb up is identical to what is was before. This will add weight to the end of jumps, and to falling. The way this works is if your y-velocity is negative, your Rigidbody2D's gravityscale parameter is ramped up.
In addition to this jump change, I reworked ground detection. The raycast for ground detection now starts just above the player character's lowest point on their Box Collider 2D, and has a very small range. This means if you're chest-deep in the ground somehow, and your feet are dangling in space, you can't jump. This is seen in the one-way platforms we have, since we want to wait until we're standing on them to jump off them. The other small change to make this work is to make sure our y-velocity is <= 0. If it's greater than 0, we are in the upwards arc of a jump, and shouldn't be able to jump off a platform yet, since we haven't landed on it.
First off, I greatly improved the hitbox system. The player's hitbox detection is going unchanged for now -- when the player is hit, they get invincibility frames (iFrames). Enemies do not get iFrames, but shouldn't be hit by the same attack twice during its duration. The reason we forego iFrames on enemies is that we want to be able to combo them with attacks one right after the other. It wouldn't make sense to a player if they swung at an enemy, hit them, took another swing, and that one missed. If also wouldn't make sense if a player swung at an enemy, and hit them 20 times with that one swing. We expect to hit them once.
In order to meet this criteria, I was just using Unity's OnTriggerEnter2D function. This causes issues when hitboxes re-enter an enemy's hurtbox during the same attack animation, as it'll hit them again. If the hit knocked an enemy away, and the player was moving in that direction, re-hitting the enemy was a relatively common occurrence.
To improve upon this, I modeled a new detection system off of what I assume Super Smash Bros. does. Upon being hit, the hitbox is stored in a queue. Whenever we are hit again, we check the queue to make sure that the hitbox touching us isn't in the queue. If it is, we simply ignore the collision. This allows us to call OnTriggerStay2D, which means that if a deactivated hitbox spawns inside the enemy (for some reason), it will properly hit the enemy once it is activated. Additionally, this system works because hitboxes are instantiated, so even if you're using the same attack over and over again, a different hitbox object is being stored in the queue each time.
Another big improvement I made to combat this week was giving hitboxes knockback angles. When you hit an enemy, the hitbox assigns a direction for the enemy to travel during hitstun. Originally, this was just a boolean that told them to move either right or left. With this change, we can hit enemies into the air, which will make combos more satisfying.
A natural issue with this kind of system is that knockback angles are interpreted as the same direction whether we're on the right side or left side of the enemy. To remedy this, I just used that left/right boolean from before, and if it should hit the enemy to the left, I invert the x-value of the knockback angle before applying it. This also means all hitboxes should be created with the intention of hitting the enemy from the left side (trying to push them to the right).
A fun addition to this was a spike. This is a special downward angle in platform fighters that has an interesting property when grounded: bouncing! Basically, spikes function slightly differently from most hits. In the air, getting hit by a spike will just send you down to the ground. However, if you're already on the ground, a spike will send you up into the air, and will generally lock you in hitstun for a bit longer, giving the one who hit you some extra time to follow up and create cool combos.
Really Basic Tools
Camera Bounds Display
I wanted a tool to show the bounds of our Camera in any given scene, so we know exactly what the player will be able to see as they run around the room. This dynamically updates along with any of the 4 floats that define the bounds of the camera.
The light-blue (cyan) box around the scene is the maximum view of the camera. As the camera travels and reaches its boundaries, it won't ever see anything outside of the light-blue box.
This is based on a tool I built in a previous prototype, which I wrote about >here<
Hitbox Angle Display
I also wanted a tool that shows the knockback angle you're creating on a hitbox as you edit it. This is hardly necessary, but its implementation was very simple so I knew it'd be time well spent (3 minutes).
In this image, the green box is the BoxCollider2D of the current attack's hitbox. The red line is the angle that an enemy will travel after being hit by this attack. Making that red line appear (and update in real time as we edit the knockback angle) is all the tool does right now.