• Andrew Jonhardt

Masks of Undying Update #4

I'm making this post early in preparation for corrective eye surgery next week. The procedure has something like a 99% success rate, so I believe my eyes will recover just fine. However, the recovery period is at least 1 month, and I've been informed I'll need to avoid stressing my face muscles for that month. No squinting, no eye rubbing, and no scrunching up my face, which basically means I need to avoid hard exercise, reading when I'm tired, and heavy computer use.


I'm going to reduce the amount of time I spend working on Masks of Undying. Programming, while an enjoyable struggle, provokes all of the facial behaviors I need to avoid. However, I have a little time before I undergo the procedure, so I will be putting more work into Masks over the next few days.


I will not be making a blog post next week, so as to help with the healing process. However, I will return to blogging the week of the 11th.


So, what is the current progress of Masks of Undying?


I'm sad to say the 1-asset completed per weekend pace I was previously at has slowed. There's 2 reasons for this:

  1. The first enemy I created, the Shuffle, has become something of a template for all other enemies.

  2. I've been trying to pursue math-y solutions to my problems, instead of easier-to-implement collision-based solutions.


When I started Masks, I wasn't sure an enemy template, or an enemy that has attributes that are referenced by all other enemies, would be appropriate. The majority of enemies attack in different ways, and I initially thought I'd have them all moving differently. However, over the course of creating Shuffle, I realized I wanted every enemy to have an idle phase before the player is "seen", and that this phase could be the same for most enemies.


An idle phase would be a departure from my inspiration, Splatterhouse, in that enemies would not initially react to the presence of the player onscreen. Enemies would instead wander around in set cycles, until such time as they are alerted of the player's presence. Following Splatterhouse's design would mean setting all foes to immediately rush the player as soon as they appear in a level, and I don't find this idea to be particularly interesting. So, I'll be giving each enemy an Area2D and CollisionShape2D node to check proximity to the player instead.

If the player enters the big blue circle, the enemy at the center of the circle with activate and begin trying to hit the player. If anything else enters the circle, the enemy is currently set to do nothing.

With a proximity-checker, literally just a signal from the Area2D to a script to confirm if the player ever enters the Area2D, enemies only respond when the player is in range. This allows for staggered encounter choices, or the ability for the player to self-direct how, or even if, they'll encounter certain enemies throughout a level.


Now, in my opinion, the way I'm using Shuffle as a template is not correct. Template enemies are typically an object that isn't supposed to be used; it's a collection of all of the common qualities you know all of your enemy types will share. In this case, the template would be an enemy with all of the required collision zones, a script that enables movement and switching into and out of an alert state, and that's it. These shared attributes would be imported into every new enemy, and then a script would be added to build off of the template script and add attack programming.


Thankfully, the Shuffle enemy is so basic I didn't see any need to waste time remaking him once I figured out all the collision zones. I just added a Boolean switch that activates a specific attack function.


The Shuffle attack function was where the challenging math stuff started.


Let me break down the problem:

  • Masks of Undying is a top-down game.

  • Enemies must attack deliberately; only 1 kind of enemy does damage just by contact with the player.

So, before anything else, I needed a way for Shuffle to sense the distance between the player and itself and the angle the player was currently at. Distance is easy (though I don't know enough to explain why getting the length() of heading works for this):

heading = Global.Gamestate.player_loc - self.global_position

distance = heading.length()

In the above, I'm pulling the stored player position from a global script, so that's what Gamestate.player_loc is.


The hard part was figuring out the angles. See, the function rad2deg spits out degrees, but the degrees it provides don't correspond to what I'd expect for a circle or square. There's even negative degrees. So, after alot of troubleshooting, I finally arrived at the following:

var angle = rad2deg(self.position.angle_to(Vector2(Global.Gamestate.player_loc.x - position.x, Global.Gamestate.player_loc.y - position.y)));

var left = range(102,164)

var right = range(-18,-80,-1)

var top = range(-106,-172,-1)

var bottom =range(11,74)

for i in left:

if i == int(angle):

active_movespeed = 0

$Arm.position = Vector2(-10,16)

$Arm.rotation_degrees = 90

fired = true

else:

pass

for i in right:

if i == int(angle):

active_movespeed = 0

$Arm.position = Vector2(40,16)

$Arm.rotation_degrees = -90

fired = true

else:

pass

for i in top:

if i == int(angle):

active_movespeed = 0

$Arm.rotation_degrees = 0

$Arm.position = Vector2(16,-10)

fired = true

else:

pass

for i in bottom:

if i == int(angle):

active_movespeed = 0

$Arm.rotation_degrees = 0

$Arm.position = Vector2(16,40)

fired = true

else:

pass


The angles here are weird. I don't understand why the top of Shuffle is considered degrees -106 through -172 (the -1 is used to iterate backwards through a range, and you can't use negative numbers in a range in Godot without it), or understand the ranges I had to use for all of the other directions. I do know the numbers on the outside of the ranges, 102 to 164 for left, for example, are approximate to the size of the arm the Shuffle uses to punch at the player. Outside of that, I'm over my head here.


The rest of the script is for iterating through the provided degree ranges when a player is detected as in range (the range() function is just an array, so you have to process it using loops like "for"). If the player is at a degree provided in the range(), the Shuffle stops in place and throws out a strike. A separate function, tied to the "fired" Boolean, starts a countdown and then unfreezes the Shuffle after the timer runs out.

if fired == true:

fired_timer -= 1

if fired_timer <= 0:

fired = false

fired_timer = fired_timer_ori

$Arm.position = Vector2(14,16)

$Arm.rotation_degrees = 0

active_movespeed = base_speed


I've realized I'll need a timer at the start if I want to have Shuffle strike in 8 directions instead of 4. The current attack is so fast that only the most skilled players could hit a Shuffle without getting hit first. Altogether, this is almost enough to make me give up having 8-directional attacking. However, Moonlighter showed me how weird it feels to have only 4 attacking directions when you can move 8 directions, so I will be instituting 8-directional attacking for the player at the very least.


So, I finished an initial version of Shuffle, even if it can only attack in 4 directions currently, and by extension I finished a 2nd enemy that's just a harder Shuffle. The next math-bit that slowed me down was for an enemy I'm calling Anger.


Anger likes to charge at things. It's what he does. However, in order to charge, he has to figure out if he's facing the right direction. He has to know what direction he's currently facing, and how his facing direction relates to the player's current direction. After extensive research, I stumbled across something called a dot product.


To put it simply, a dot product is a direction test that always spits out a value between 1 and -1 depending on the directions you and another object are facing. This is the example Godot provides:

var AP = (P - A).normalized()
if AP.dot(fA) > 0:
    print("A sees P!")

Fuck, I want that script formatting... anyway, the problem here is that the Godot documentation doesn't really explain fA in a way I can understand. P is player and A is attacker.


After extensive Googling, I found out that fA is the vector from the enemy to the player. I tried my existing direction scripting to try and solve for fA, but I always got back either only 1 or only -1 with no variation according to position.


I spent a day trying to figure out dot product. I'm certain I'll need it for the future. However, for Masks, I opted to throw out dot product and instead implemented a raycast solution that only took 10 minutes to set up and get working.


The ray only collides with the collision layer the player is on (a Godot feature), and then triggers Anger to charge. If he doesn't see the player, Anger will start searching by spinning in place. I'll probably need to add random movement in future so the spinning doesn't look too weird.


This was the longest post I've written in a while. Hope you enjoyed it.


Until the week of the 11th.

© 2023 by Andrew Jonhardt. Proudly created with Wix.com