Archive

Programming

Cameras are hard. If you've ever found yourself fighting against a camera that would really like to pivot in a direction you don't want it to, you've been on the receiving side of this technical difficulty. The reason behind your struggle is simple: spiteful developers.

Not really. Or at least probably not. The fact of the matter is that making a camera which balances the atmosphere of a game, moves quickly enough to keep up with the player, moves slowly enough to not move around with the player's jitters, and accounts for the corner cases like a player getting teleported -- it's not a simple thing.

I'd like to talk about the different methods I've been experimenting with in Clearing Skies. This is as much a journal entry for me to see what I've tried and why it works or doesn't work as anything else. I hope that someone may find some insight in it.

Approach 0: Strap the camera to the player.

This is the simplest and most straightforward approach to moving the camera. Wherever the player moves, the camera follows.

  • Simple! Easy to implement and understand.
  • Basically no corner cases where movement will be odd.

There are some drawbacks, and they're not too hard to see. When the player shakes or moves about drastically (like if she shakes after a shock or hit), the camera is liable to jitter along with her. Shaking the camera is a good way to induce nausea or eye strain in the player. Additionally, you lose the sense of level depth. The entire space is continuous. This is fine, generally, but for the first dungeon in particular, this felt to me like it shrank the space considerably.

  • Can cause eyestrain for sudden movements.
  • No way to add 'cinematic' elements on transitions between rooms.

Maybe there's an easy solution to the rapid movement problem? Indeed, we can try...

Approach 1: Smoothed Camera Movement.

The camera smoothly transitions between where it is and where the player is located. This solves a lot of the issues with the player shaking rapidly. It's like attaching a spring to a camera gimbal.

  • Still easy to implement.
  • Reduces eye strain from rapid motion.

The downsides are visible in the gif above. The biggest issue we run in to is the rapid movement of the player between rooms. The camera has to spend a few frames catching up to the player location on transport to a distant location. This can be solved by snapping the camera (disabling smooth on transition), on setting a max threshold before the camera teleports, or a few other tricks. No matter what, it's a bit more involved than simply assigning a location, and it still doesn't give us the cinematic room sweeping we want.

  • Camera has to catch up on long moves.
  • No cinematic controls.

Approach 2: Camera Zones

When the player character enters an area, the camera's bounds are set to match the region. This means the camera moves smoothly inside the zone, sliding to follow the player and 'transitioning' when the player moves between regions. I like the way this looks -- it divides the map in such a way that it makes things appear larger than they are.

  • Looks good. Cinematic.
  • High degree of control over where the camera moves. Can cut off areas outside of the map to avoid wasting screen real estate.

It's not perfect, though. Transitions are very abrupt and might cause issues with visual tracking of the player. It's also very cumbersome to implement, as the zones need to be defined manually:

Furthermore, troubles come in the way of overlapping rectangles. Not the region on the bottom where it may be necessary for two zones to overlap. Plus, when we finally leave the map, the bounds for the camera will need to change. Lastly, when we change the camera bounds, we lose the ability to smoothly transition between states.

  • Possibility of weird edge cases when moving between zones.
  • Time consuming to put together regions.
  • Not clear how to restore full camera zone when leaving dungeon.
  • No smooth transitions.
  • Visual tracking is hard on screen switches.

In a more perfect world, I'd love to be able to define small pins which act as boundary keepers for the camera, then, if a distance is exceeded, have the camera quickly tween to the player location and return to tracking. I don't have a solution for that worked out yet, however. Time will tell if I get it solved.

As a recent convert to Google Fi, I found myself in a position to find a memory tool for my new phone number.  In the interest of speed and laziness, I threw together a script which would produce all the combinations of letters that could be made from my number and dump them to a text file.  It seemed in good taste to share that script.

import itertools
digits = ['0', '1', '2ABC', '3DEF', '4GHI', '5JKL', '6MNO', '7PQRS', '8TUV', '9WXYZ']
digit_map = {k:v for k,v in zip("0123456789", digits)}
phone = "8005551234" # Your number here.
options = [digit_map[n] for n in phone]
with open("words.txt", 'wt') as fout:
  for p in itertools.product(*options):
    fout.write("".join(p) + "\n")

This will yield a text file with a list of entries like (for the above number):

T00K5L1B3H
T00K5L1B3I
T00K5L1BD4
T00K5L1BDG
T00K5L1BDH

It's worth noting that we could do away with the digit_map by just indexing directly into the digits, followed by a reverse before the output, but this solution provides greater clarity at a meager cost in efficiency.

Last week's progress is listed here https://www.josephcatrambone.com/?p=1110.

There are three things I really want to get done tonight.  (1) Characters, when they die, should go flying backwards after the last strike.  (2) I need to recursively swap out the crash-dummy yellow body parts with something that's more in line with the game's theme.  (3) I need to actually write something that ends the level.  I did make a phone booth.  (Whee!)

Let's review those P1's from the start:

  • Main menu with start/settings/quit.

Bam.  Done.  Sorta.  Quit and start work.  Definitely need to add the settings page and maybe put something other than "Title" in for the title.

  • The 'fighter body' should broadcast the "damage taken" event to child nodes so they can record whether or not they're dead.  Dead enemies should be despawned.

Also done!  Enemies indeed die and take damage.  That's handled by the controller.  I need to figure out what approach I want to use for the "die but then fly backwards on hit if enough damage is done."

  • End-of-level triggers need to fire.

The triggers fire when the player enters, but that's it.

I even had time for a little AI:

Last week's progress is listed here https://www.josephcatrambone.com/?p=1109.

I spent a while running through the game in its current state and think that it's fitting to add at least a few of the following features.

Priority One:

  • Main menu with start/settings/quit.
  • The 'fighter body' should broadcast the "damage taken" event to child nodes so they can record whether or not they're dead.  Dead enemies should be despawned.
  • End-of-level triggers need to fire.

Good to haves:

  • Motion feels very limited right now.  Jumping would go a good ways to helping that, as would having different attacked based on whether or not one was jumping or moving.
  • Perfect-time parrying.  Right now a user can cancel into a block at certain times, but there's no reward for having anything above 'okay' timing.
  • Rebindable keys would be very nice.

I know I should work on having a level completion and a main menu before adding any of the extras, and I expect I'll do as much, but I want to keep track of these items to I can refer back here and see how much I set out to accomplish and how much I actually accomplished.

It's the end of the first weekend. I managed to finish a few important pieces and numerous unimportant pieces. Here are some hiccups and what I did to work around them.

I started by making a walk cycle to get into the swing of things. Moving around is an important first step, pun intended.

Immediately after this I encountered a problem where moving left would cause the character to spaz out and flip left and right once per frame for as long as the left button was held. It turns out that Godot does not like having a KinematicBody flipped horizontally (scale.x = -1) and will override that. It was not, as I suspected, an issue with the animation resetting scale. The workaround here was to move the skeleton beneath a Node2D and flip that. It means the hitboxes won't be flipped correctly, but I'll have to keep them centered and balanced.

The next thing I worked on was analog stick input and variable speed walk cycles. I wanted to cool off a bit from that bug, so I spent a short while setting up key and controller inputs, then tweaking the scaling between the walk rate and the animation rate. I have a move speed and an animation multiplier speed. The move speed can vary by character, but the animation multiplier stays fixed. If a character moves 20 units per second, the animation plays at (20*animation_multiplier)x normal speed. I just have to play with the multiplier until the feet look right. One problem that comes from this is scaling. If the models change scale (or I change animation) I'll need to readjust the animation_multiplier. Could prove tricky.

The last issue I ran into was broadcasting events. I had originally set up the Fighter scene as a child of another Node2D, so a player node, for example, could have a script which handled input and called into Fighter to strike or block or move. This worked well enough, but when it came to detecting and broadcasting hit events, I found that selecting the overlapping areas with an area 2D (at the hit point) yielded the root nodes (i.e. player, npc, etc) and NOT the fighter nodes which could handle 'hit'. I struggled in part because Godot does not allow one to attach multiple scripts to the same node. I didn't want to put more logic into the fighting controller, but it didn't seem graceful to select all the nodes of a given type and then seek the first child with name "Fighter". The solution turned out to be flipping around the hierarchy. Instead of having PlayerNode -> FighterBody, I had FighterBody -> PlayerController. All on-screen fighters have this same root type which means I can simply have the animation call 'strike' and the event will propagate.


func strike():
for b in strike_area.get_overlapping_bodies():
if b.is_in_group(target_group) and b.has_method("hit"):
b.hit(self, strike_area, damage)

func hit(striker, damage_area, damage):
# TODO: Face correct direction.
hit_recovery_time_remaining = hit_recovery_time
animation_player.play("Hit_Front")
# TODO: Report the hit to child nodes.

All that comes together to make a nice striking system.