27 December 2020

Releasing a game for myself: Bomb the Bugs

Yesterday, I completed a project I've been working on for awhile I decided to name Bomb the Bugs. You can play it in your browser by following this link:
https://dgalga.itch.io/bomb-the-bugs

Bomb the Bugs was inspired by the Nintendo title Wrecking Crew. I played Wrecking Crew for the first time this year, and I found the design immediately approachable. I also tried playing the sequel, Wrecking Crew '98, which I did not find as approachable.

Wrecking Crew gameplay:
https://www.youtube.com/watch?v=-7F-7MtkHD0

Wrecking Crew '98 gameplay:
https://www.youtube.com/watch?v=Y0KodgKLh_Y

Wrecking Crew is a clunky arcade title. Each level is a puzzle, but you cannot advance unless you complete the entire puzzle. So, if you try to complete the puzzle in the wrong order, you can easily become stuck in a situation where you have to let an enemy kill you in order to progress. Worse, if you've managed to kill all the enemies you can reach, you have to wait for a fireball to spawn to kill you. To me, this design is problematic.

'98, on the other hand, embraces the matching design of most popular puzzle games and is turned into a cluttered competitive title in the process.

There are obvious elements shared by both titles. However, I didn't feel that '98 was a true sequel, and the clunky design of the original title still feels ripe for improvement.

I've been tinkering with this design ever since I first played it this year. Initially, I was frustrated by an inability to change Wrecking Crew into something I thought was worth money. When I finally did come up with something, it was a title that would require either more advanced programming skills than I previously possessed, or 3D.

The idea was this:
In the wake of the Chernobyl disaster in 1986, crews were sent out into the area surrounding the exploded nuclear power plant in order to try and contain the disaster. People were evacuated, pets and animals were slaughtered, the top layer of the ground was buried.

The game idea I had was to be set in similar circumstances. Only, replace Chernobyl with a large-scale biological disaster stemming from a chemical plant. The player would be a member of a team sent out to destroy human habitations as part of efforts to ensure people and animals infected by the disaster were cleared or put down.

The game design would need either fake 3D a la Fez, or a fully 3D environment. The dream gameplay would be a puzzle-take on the destruction from Red Faction Guerrilla:
https://www.youtube.com/watch?v=H5bv4RIOc_s

I couldn't manage that on my own, though. I didn't investigate going 3D, as I'm still shying away from the 3rd dimension in all of my games, and I wrote off Fez's method of fake 3D due to the technical skill that was required.

Now that I think back on it, there's no reason I couldn't try doing 3D with sprites someday. Instead, I settled on a method of using an array to generate a map of tiles when a game first starts:


var wall_types = ["X","H","O"]
var walls = {wall_types[0]:preload("res://Walls/BaseWall.tscn"),
wall_types[1]:preload("res://Walls/Ladder.tscn"),
wall_types[2]:preload("res://Walls/BustedWall.tscn")}
var level_map = [
"XXHHOXXHHXXOHHXX",
"XXHHXXXHHXXXHHXX",
"XXHHXXXHHXXXHHXX",
"XXHHXXXHHXXXHHXX",
"XXHHOXXHHXXOHHXX",
"XXHHXXXHHXXXHHXX",
"XXHHXXXHHXXXHHXX",
"XXHHXXXHHXXXHHXX"
]
var map_height = -1
var map_length = -1
var map_test = 0
var map_pos = [] #array of map positions
var map_replacement_pos = []


func _ready():
     fullscreen_sprite.visible = false
     for line in level_map:
         map_height += 1
             for chr in line:
                 map_length += 1  
                 if map_test != map_height:
                     map_length = 0
                     map_test = map_height
                 wall_instance(chr)

func wall_instance(wall_type):
    var wall_inst = walls[wall_type].instance()
    behind_player.add_child(wall_inst)
    wall_inst.global_position = Vector2(map_length,map_height) * tile_size - \
        build_point_zero
    if wall_type == wall_types[0]:
         map_replacement_pos.append(wall_inst.global_position)
         map_pos.append(wall_inst.global_position)


I above code is still kinda cool to me, though overall it's not that special. It's GDscript that takes an array of lines like
"XXHHOXXHHXXOHHXX"
and compares these lines to a filter of expected wall types. In this case, X = basic wall, H = ladder, and O = a point the enemy units can spawn from. This allows you to easily spawn in an array of tiles of whatever kind you want, in this case Area2Ds, instead of needing to rely on Godot's occasionally unhelpful built-in Tilemap system.

I couldn't make a game within the parameters I set for myself that was worth money (imo) and better than Wrecking Crew while embracing that original design influence. So, I eventually decided to change the design.

I found myself picturing a city of sentient buildings, with the player acting as one of a limited number of agents with the ability to cleanse these buildings of infestation. That's where the "O" in the above line comes from: I was picturing a game where, if the player took too long, enemies would burst from previously uninfected wall panels and increase a global countdown to failure.

I still have the bugs bursting from walls, but the longer this year went on the less I was sure about the rest of it. I resolved to build the most basic, lazy version I could. And, I've now succeeded:
https://dgalga.itch.io/bomb-the-bugs

This year has been hard. It's also been the most productive year I feel like I've ever had. I'm still a long way off from where I want to be, and I'm honestly not sure what to do with my time as the deadline for when I resume school looms closer.

Starting next month, I'm going to try evaluating games posted in the r/PlayMyGame subreddit. I've been frustrated by feeling disconnected from the game making community at large, and game jams haven't done much for my need to discuss design. The boardgame designer group I frequent is no longer a complete substitute either. So, I'm going to try playing other's games and giving my feedback on them. I'll post another entry next month accordingly.

Until then.

13 December 2020

Spakegames.com is officially over

I've transferred over all the articles I care about from my Wix blog, in the process learning that I was on the yearly plan instead of a monthly plan. So, I won't actually be saving any money this year from canceling my blog. Cool! On the plus side, the price of my plan looks like it was increasing by nearly $20 per year, so happy I won't have to worry about remembering to cancel it next year.


The SpakeGames pages have all been deleted, the billing has been shut off, the business email gone. I won't save as much money as I'd like, but I'm happy to shut down what was effectively just an experiment. 

 

Blogger isn't fancy, and I don't think I'd want it for a business, but it's good enough for my needs and directly exposes HTML code. It looks like, from my basic searching, that I should be able to run Javascript directly on my pages. This would mean I could hypothetically run Javascript games directly in posts! I'm not sure if I'll actually do this, but it's food for thought.


I'm planning to take the rest of the day off. I'll post again in future at some point. Not sure when. At the least, I've been working on a game about dropping bombs on bugs that I hope to finish soon. So, until then!



Punch in the Dark development archive

 7/30/19:

I can't continue with my plan of making Pointman. Not yet. This much was obvious after how much I struggled on Text to Ghost last week. So, what can I make instead?

I had given up on this question, and was busy exercising, when I found myself listening to a song that I had forgotten was on my phone: (omitted video of Carpenter Brut's Cheerleader Effect)
 
 And, I found myself thinking of a game that once inspired me: (omitted video of Splatterhouse 1)
 
 I once played a Splatterhouse cabinet during the yearly Fantastic Fest in Austin. I had never seen the game before and, despite the clunky jumping and a level of difficulty designed to eat currency, I saw something special in the surprisingly entertaining yet simple design.

The main character in Splatterhouse, Rick, is a brawler, but he's not as fancy as Ryu from Street Fighter. If you stand and attack, you get a punch. If you jump or crouch before attacking, you get kicks. There's a slide move I still haven't mastered. The foes Rick faces are often just as basic, with the difficulty coming from the varied directions of attack combined with the hazards and traps that fill the levels.

The point is, when I look at Splatterhouse, I see a game even I could make. And, if things were different, a game that I would be happy to have made. This is not an impression I get from most games, and I decided it was grounds to use Splatterhouse as a guide.

My next game will be a commercial product, influenced by Splatterhouse, with likely contracted art assets. I'm scrapping my previous time table and associated plans, as they were intended for Pointman, and I will instead be allocating 6 months for development of an initial prototype. If I can't get an art-less prototype done in 6 months, I'll drop the project and go back to free, hobbyist productions. If I do get a prototype done in 6 months, however, I will begin searching for a composer.

I've already met someone whose stated a willingness to serve as an artist for the project. Getting an artist this early holds alot of potential positives and negatives, so I've given myself a test to complete first:
I've got until Friday to assemble an initial list of all of the sprite, animation, and 1-panel "cutscene" assets I will need for the game.

If I can assemble an initial asset list, I will use the number of assets as a basis for a quote for the artist's services. If I cannot assemble the initial asset list in time, I'm clearly not ready to be seeking out artists and will postpone seeking an artist until I'm much further along.

I'll have more to say about Project Splatter next week.


8/8/19:

My efforts this past week have been focused on 4 things:
Scheduling.
Planning assets.
Coordinating an art test.
An initial version of the main character for Project Splatter.

I actually started with the asset planning, oddly enough. Project Splatter has been bouncing around in my head for years, and I'm fairly confident in what I want:
  • Top-down 2D perspective
  • 16 levels
  • 5 Bosses
  • 9 Base foes
  • 5 environmental hazards
  • Powerups
  • 1 Attack
  • 8 directional movement
  • The ability to dodge instead of jump
Working from this baseline, and in combination with a very basic game design document, I've built out a list of (currently) 1035 assets. Floor tiles, with variation, sprites to be assembled into animations, and a handful of 1-panel scenes to be combined with text to serve as cutscenes.

I'm doing my best to plan for reduced cost in production, but I also know it's too early to be betting on the final asset count. The point is the experience of pre-production, something I don't think I've ever attempted on a project before. Even Redline was born of an existing board game prototype, and couldn't effectively be planned out due to my own questions regarding my Unity skill.

The biggest aid in figuring out how I want pre-production to go has been the potential for a collaborator, even if that collaborator is essentially a contractor. If Project Splatter were just me, I'd care far less about scheduling and just get started. Trouble is, especially when money is involved, half-assed scheduling feels like a recipe for pain.

The art test I assembled for my potential artist is very basic. It was simply extrapolated from my existing needs. Sprites, tiles, a big scene. I've been working with him to confirm what I want from the test, and to iron out confusion points. Even if our collaboration fails to materialize, I'm confident we're both learning a great deal from this experience. He's learning more about the technical side of game assets, and I'm learning how the heck to explain what I'm looking for.

In addition to all of the scheduling and art-planning, I took time out to begin prototyping my main character. I, of course, want him to feel good to drive. I don't think I'm quite there yet, but I got enough done this last weekend to move on to the next set of prototype assets.

Meet Jack:

At the moment, Jack can:
Move in 8 directions.
Punch in 4 directions.
Dodge (that red flashing) in 4 directions.

Punching took a bit of effort. I want 1-button punching, but there's 8 directions to deal with, so I've got Boolean variables tied to the current direction Jack is moving in. If he's moving forward, he'll punch forward, etc. Eventually, I'm going to expand on this system to allow Jack to punch in 8 directions.

The biggest headache in getting Jack working has been the dodging. I originally wanted dodging to be tied to the movement keys through double-taps. Tap, say, the A or Left Arrow key on the keyboard twice, and Jack would suddenly dodge left.

After some investigation, I've decided double-tapping may not be the way to go.

Firstly, double-tapping is a bigger technical headache than I expected. It's possible to jerry-rig something with a bunch of timers and Booleans, and I got a basic version of such a system working, but the result took a ton of work and was really bloated, ugly code.

Secondly, there's a community of people who hate double-tapping in PC action games. For some, it's a question of preference. For others, however, double-tapping is a physical hurtle that impedes accessibility.

My current version of dodging relies on holding down 1 extra button while pressing a movement direction. Some additional work will be required to allow dodging in 8 directions, but the work isn't what worries me. What worries me is that holding down 1 button doesn't feel good.

In every other action game I've ever played with a dodge option, the button you use to dodge is simply a block button whenever you hold it down and don't move. It's only when you attempt to move while holding the block button that you realize you can roll or whatever. Still other action games, console games, map the roll function to the left stick on a control pad.

I don't like either of these options for my game. I wanted to keep things simple: attack or dodge. No standing in place, no pretending you're safe, always moving.

I'm considering the idea of a default dodge direction you can alter by holding movement keys. I'm not convinced this would provide players with the most accurate option. However, regardless of my feelings and wish to experiment, I need to move on.

This weekend, I'll begin prototyping player tracking and the first in-game enemy. The plan is 1 to 3 large programming goals/foe prototypes per weekend for the next 7 months. If all goes well, this prototype will be done by the end of February, and I can immediately transition to beta testing and bug squashing.

Until next week.
 
 

8/14/20:

I've got good news and bad news. Good news first.

I'm 1 weekend ahead on my project. The three initial challenges I set aside for myself, making a callable function for player damage (to be filled out when damage becomes important), handling players falling off levels, and re-spawning the player after they've fallen, proved so trivial to implement that I completed all 3 in about 30 minutes. Holy fuck.

The reason I'm only 1 weekend ahead on my schedule is because the 3 challenges I assigned myself for next weekend, the next 3 traps (I consider tiles that can force a player to fall to be "traps"), proved tricky to implement. The 3rd trap in particular was directly changed from the original design in favor of something better fitting my goals.

Originally, I intended for one of my traps to be a "shot trap". Designed to shoot out projectiles at regular intervals, the shot trap would provide an obstacle capable of impeding huge areas of player-passable terrain. Unfortunately, this kind of trap presented 2 problems I wasn't a fan of. 1st, extra sprites would be needed to align with all 4 wall directions, adding cost to a budget I've been working to keep tight. 2nd, every aspect of the shot trap, from the projectiles to the trap location and attack angle, would require a higher level of development investment to ensure the player remains aware of the danger when in melee with foes on all sides.

This is what I implemented instead of the shot trap:
I call this the push trap. It's big, so it's hard to miss. I can re-use and alter the coloring of existing wall assets to define its appearance, thus ensuring no sprite budget increase. I could do even more in code, and have the push trap vibrate or shake before launching itself at the player.

I say "could", because I didn't have time to get any of that stuff working. It took all of my free time on Sunday just to get to what you see now.

When I started on the push trap, I wanted to use a Raycast to check when the player was in front of it. Raycasts in Godot couldn't get me the information I wanted, so I had to settle for a collision box (the light-blue box the player crosses to activate the trap).

The next issue was actually just making the push trap push the player. As you may be able to tell from the gif, this remains an unsolved problem. When the player comes up from below the trap, the trap correctly pushes the player. However, when the player comes down to the trap from above, the trap moves both forward and upward instead of just straight, and then gets stuck at half the distance it was supposed to travel!

I believe the weird behavior in the push trap is due to a quirk in the way pivot points work for nodes in Godot. All nodes have a pivot point in the upper left side, and this pivot point may only be moved on visual (sprites, color blocks, etc) nodes. So, unless I want a sprite to be the root node for my player (not advised), I'm stuck with scripts that are getting the upper left portion of the player instead of tracking the player object from his center.

This is the way I understand it, anyway. I could easily be wrong.

This is the code I used to try and move the trap in a straight line if the player is present:
func _physics_process(delta):
if player_in == true and return_time == false:
move_direction = get_direction(player_pos) * move_speed
self.translate(move_direction)
elif player_in == false and return_time == true:
move_direction = get_direction(ori_pos) * retreat_speed
self.translate(move_direction)
if position == ori_pos:
return_time = false
position.x = clamp(position.x, -ori_pos.x, ori_pos.x)
position.y = clamp(position.y, -ori_pos.y, ori_pos.y)

If the player is in front of the trap, I get the player's position and turn it into a direction to move the trap toward the player (regardless of angle):
func get_direction(player_pos):
var heading = player_pos - self.global_position
var distance = heading.length()
var direction = heading.normalized()
return direction

The above direction-getting formula was the biggest headache out of the whole weekend! This is because the latest version of Godot simply offers a .direction() built-in function, but I'm not on the latest version and my past experiences with Unity have dissuaded me from upgrading my engine versions during a project. Ultimately, I was unable to find a Godot-specific workaround, and I ended up trawling for Unity C# code I could convert. I eventually found something like the code above. Well, the original version actually looked like this:
func damage_recovery(position):
var heading = position - player.global_position
var distance = sqrt(heading.x*heading.x + heading.y*heading.y)
var direction = heading / distance
player.position.x -= (hurt_bounce*direction.x)
player.position.y -= (hurt_bounce*direction.y)
It was only after I completed the above code (which I'm still using as part of my player damage function to bounce the player away from incoming damage) that a Reddit user by the name of Cykyrios pointed out several corrections.

It's silly to have 2 different direction-finding functions in my game, I know. However, considering both scripts do different things, I've decided to retain both for the time being to avoid breaking anything.

Despite how much trouble the push trap caused me, I was in high spirits until I was reminded of something. Here's my bad news:
I made an agreement with my current employer to complete a certification before the end of this year, and I'm only half-way through the training I've been doing.

In order to obtain the certification I promised to get, I will need to step away from this project for a month to study and test. I'm hoping the process won't take more than 1 month. Additionally, in order to avoid wasting my own money, I will need to pass the test the 1st time I take it.

As of now, I'm allocating September as my study month. I've got less than 10 hours of coursework remaining, so the only difficulty will be forcing myself to pay attention. And, finding a testing location. And, testing. I'm not looking forward to it.

Until next week.
 
 

 8/21/19:

This past weekend I decided that, more than anything else, I need to get my certification promise to my employer out of the way. Accordingly, I did absolutely no game design. I didn't want to risk the temptation to abandon my responsibility.

In lieu of design, I spent my study breaks playing videogames, watching TV, and generally flashing back to my college and high school days. In regards to videogames, my focus generally centered around 1 title.

Moonlighter is a top-down, 2D action game with rogue-light and RPG elements. You play a merchant and adventurer from a town that sprang up on the edge of a network of dungeons. The previous sentence is literally all you need to know about the game's backstory, yet you'll be treated to some of the most extensive and pointless backstory when you start the game.

1 half of Moonlighter is combat, and the other half is store management. I'm not a fan, I only stomached about an hour of the game, but I was drawn to it for how the combat resembles what I want for Project Splatter.

Movement in Moonlighter is 8-directional (up, down, left, right, up/left, up/right, down/left, down/right). You have a dodge, a primary attack, and a secondary attack. You can only dodge and attack in 4 directions (up, down, left, right). Dodging is tied to one button, and if you hold the dodge button and a movement direction at the same time you will dodge in the given direction. If you press the dodge button without holding a movement direction, you will dodge the default direction of backwards from your current facing direction. The dodge appears to allow for a slight tilt in movement when you hold, say, up and left, so dodging is almost 8 directional. Attacking, however, is purely 4-directional.

I found the navigation and combat system to be inspirational. It confirmed for me that the dodge system I want to make can be fun, that adding a frame or two of invincibility to a dodge feels good, and that adding a default dodge direction can help rather than hinder. Additionally, the 4-directional dodging and striking confirmed how weird 4-directional limitations feel in an 8-directional movement game. To be sure, the strikes in Moonlighter are either wide enough to hit foes at an angle or long enough to keep foes at a sizable distance. Still, the need to occasionally line up attacks was just enough extra effort to bother me a little.

The systems outside of Moonlighter's combat are the reason I lost interest in the game. There's the backpack, for starters, into which you will collect junk from around the dungeon and from defeating enemies. There's so much junk that your backpack will quickly fill, encouraging you to return to town and your shop. The backpack can be annoying with how quickly it fills, but this in itself is not a problem.

The system for returning to town in Moonlighter is exactly what I wouldn't want for this sort of game.

Firstly, you don't appear to have the ability to return to town whenever you want. The return system is represented in the UI with a mirror. The first time I used the mirror it flashed at me when I activated it. When I returned to the dungeon, the mirror continued to flash at me every time I entered a new room. However, I couldn't get the return system to work again until after I'd defeated the first game boss. This meant the UI was giving me confusing information by flashing at me, my backpack was filling up, and I had no way to leave the dungeon or otherwise do anything about it.

Secondly, there doesn't appear to be any way to return to the dungeon from where you left. If you leave the dungeon, you get to start over. I always find this sort of thing frustrating, but it's not a deal killer on its own.

Aside from the system for exiting dungeons, the town your store is in, as well as the store itself, also bothered me. The town in particular felt pointless as the NPCs provide nothing but flavor. Gameplay-impacting interactions (purchases from members of the town) seem to occur exclusively in a menu. The whole town could just be a menu!

Your store isn't much better. The system of putting the items you collect from the dungeons on display is cute, at first. You'll have 4 items out at a time, though you can have a stack of up to 10 of an item on 1 spot. When you open your shop, NPCs will come in and look at your junk. However, before you can sell your junk, you have to guess the price.

The main character, Will, has absolutely no idea how to estimate the cost for items. Yes, despite being a merchant. So, when you set items out for sale, you'll have to guess the initial price for every single item you find.

Price guessing isn't necessarily bad on its own. It can even be fun, when guessing the price is some of the most work you do. The problem is all the extra work Moonlighter tosses on top of the guesswork.

First, there's the busy work of waiting for NPCs to make up their minds and assisting with NPC sales. These portions felt like they needed an automation option, especially when I constantly had to run over to the item display area and swap or add items.

Second, there's the NPC emote system for feedback. If NPCs have a sad face when they look at an item you have on sale, it's priced too high. Neutral-happy, you've hit a good baseline. Super-happy with money for eyes, the price is too low. Angry... I have no clue, and it's not really explained anywhere I could see. Too many stacks of an item, maybe? The NPC is mad they can't pay? I've also had NPCs give me different feedback on the same item. Is feedback literally NPC-based, so you have to constantly guess?

Overall, I found the feedback system for price guessing to be incredibly unhelpful. The investment needed in figuring out what NPCs wanted made me want to experiment with single items at a time. Yet, you're limited by in-game daylight, you've only got so much storage space for your dungeon junk, and fighting in the dungeons was far more fun than the store headache. The problem is that you need to sell items to make any money.

Time and time again, I found myself wishing for a junk bin to toss items into. Dungeons do have something like a junk bin, but it's only in select areas.

Moonlighter isn't a bad game. It feels underworked in gameplay and overworked in plot, and bothers me in all the wrong ways.

I wanted to have gameplay from Moonlighter for the above blog post. Unfortunately, the sound is crap and I'm now out of time for screenshots.

I'm going to continue analyzing games for this blog until I've passed my certification. I hope to be done with the studying in 3 more weeks, leaving a 4th week for the actual test. The sooner I get this done, the sooner I can get back to design. Is what I keep telling myself.

Until next week.
 
 

 10/18/20:

This week has been so busy I almost forgot to make a blog entry. Again. I'm very much hoping it'll slow down soon, cause I haven't even had time for a haircut!

My original topic for this week was going to be color palettes. I was supposed to learn about them in college, but I was never sure I understood how exactly one chooses the colors in a palette. I still don't believe my understanding is completely accurate, but a little research has (mostly) cured me of the notion that color palettes be some artist black magic.

In the broadest terms, a color palette is what happens when you combine the colors you know you want with color theory. I don't actually know anything about color theory; I've been using this free Adobe Color palette generator thing to compensate.

Now, you might be asking why I'm so interested in colors all of a sudden. Well, I've decided to see what it would take to do all of the art in Masks of Undying myself. Queue booing noises.

I've got 3 reasons for this switch:
  1. Doing the art myself is the (monetarily) cheapest option possible.
  2. GraphicsGale makes art generation stupid-easy and kinda fun (though I may need to use another program for frames of animation).
  3. I don't believe Masks of Undying is going to make me alot of money. I definitely don't believe I'll be recouping any expenses of time and/or money. I don't have the expertise to generate and maintain a community and, without a community, indie developers tend very strongly towards failure with their first projects.

The 3rd reason is actually an expectation I've had from the beginning. However, I was initially willing to drop money on the art because I'm not an artist, I've got a safety net in the form of my full time job, managing a relationship with a contractor would be good management experience, and I'd very much like to complete my game in the smallest timeframe possible. All of these reasons made alot of sense before I started messing around with GraphicsGale.

I'm not going to pretend GraphicsGale is amazing or anything, because it's very much not good at alot of what other paint/photo tools do. However, GraphicsGale is so easy to use, and can be acquired so easily (I highly recommend the portable .zip over messing with any installer), that I've found myself tinkering with it in moments of downtime.

Using GraphicsGale, I've generated black and white outlines for every one of the basic foes in Masks. This inspired me to look into colors, of which I've so far generated 150 from a base of 5 color codes:
Worn jeans = 98c4e0
Blood = 8a0303
Slime = 65ff00
Old pig skin = E3C1CD
Steel = CACCCF

By working in my spare time on weekdays (weekends remain for programming and game design tasks only), I'm aiming to complete the majority of the art that Masks will need. There may still be assets I need to pay someone to make, music for example, but I'm going to try and use paid labor as a supplement to my own efforts.

Next week, I'm planning to produce a blog entry before Friday. Oi.
 
 

 10/27/19:

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.

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.
 
 

 4/5/20:

Per my last post, this blog entry will be covering my implementation of the main character's attacking limb in Mask of Undying. Technically he attacks with a punch, but I've been calling it "the arm" since this project first started so I'm sticking with that for consistency.

extends Area2D #parent direction var right var left var up var down var punch #position-impacting variables var offset = 24 var knife_offset = 16 var board_base_off = 24 var board_comb_off = 20 var p_time var ori_p_time = 10 var punched = false #children (items) var knife var board
Implementing attacking limbs has always seemed oddly difficult to me, and my list of attack-affecting variables reflects this. It's probably just me.

Now, as you can probably tell from the above, the Arm is just an Area2D. This means it is incapable of pushing anything without such interactions being manually programmed, and that the arm will pass through walls and other "solid" objects. It's true that you can use a kinematic2D body instead, because a kinematic object will still report collisions and will be stopped by solid objects. However, I've also taken the step of nesting the Arm within the player object.
In order to avoid physics complications, and to ensure an easy setup, I believe manually programming all Arm interactions will be preferable for the long term.
func _ready(): p_time = ori_p_time #convert inputs in parent to actable bools func get_parent_var(): right = get_parent().right left = get_parent().left up = get_parent().up down = get_parent().down punch = get_parent().punch
The _ready function sets up a timer, and the get_parent_var function works to ensure the direction the Player is facing is always tracked and passed to the Arm.

func item_correction(): #this function corrects knife and board pos when weapons should be active if knife == null: return else: #the below mess repositions the knife when ya punch if punch: if right and !left and !down and !up: knife.position.x = knife_offset #modify X despite rotation + 16 because of adding #onto hand's pos. if knife.rotation_degrees != 90: #should be able to use 1 rotation for #left and right knife.rotation_degrees = 90 elif left and !down and !up and !right: knife.position.x = -knife_offset #modify X despite rotation - 16 because of adding #onto hand's pos. if knife.rotation_degrees != 90: knife.rotation_degrees = 90 elif up and !left and !down and !right: knife.position.y = -knife_offset if knife.rotation_degrees != 0: knife.rotation_degrees = 0 elif down and !left and !up and !right: knife.position.y = knife_offset if knife.rotation_degrees != 0: knife.rotation_degrees = 0 elif left and up and !down and !right: knife.position.y = -knife_offset knife.position.x = -knife_offset if knife.rotation_degrees != -45: knife.rotation_degrees = -45 elif left and down and !up and !right: knife.position.y = knife_offset knife.position.x = -knife_offset if knife.rotation_degrees != 45: knife.rotation_degrees = 45 elif right and up and !down and !left: knife.position.y = -knife_offset knife.position.x = knife_offset if knife.rotation_degrees != 45: knife.rotation_degrees = 45 elif right and down and !up and !left: knife.position.y = knife_offset knife.position.x = knife_offset if knife.rotation_degrees != -45: knife.rotation_degrees = -45 else: knife.rotation_degrees = 0 knife.position = Vector2.ZERO if board == null: return else: if punch: if right and !left and !down and !up: board.position.x = board_base_off #modify X despite rotation + 16 because of adding #onto hand's pos. if board.rotation_degrees != 90: #should be able to use 1 rotation for #left and right board.rotation_degrees = 90 elif left and !down and !up and !right: board.position.x = -board_base_off #modify X despite rotation - 16 because of adding #onto hand's pos. if board.rotation_degrees != 90: board.rotation_degrees = 90 elif up and !left and !down and !right: board.position.y = -board_base_off if board.rotation_degrees != 0: board.rotation_degrees = 0 elif down and !left and !up and !right: board.position.y = board_base_off if board.rotation_degrees != 0: board.rotation_degrees = 0 elif left and up and !down and !right: board.position.y = -board_comb_off board.position.x = -board_comb_off if board.rotation_degrees != -45: board.rotation_degrees = -45 elif left and down and !up and !right: board.position.y = board_comb_off board.position.x = -board_comb_off if board.rotation_degrees != 45: board.rotation_degrees = 45 elif right and up and !down and !left: board.position.y = -board_comb_off board.position.x = board_comb_off if board.rotation_degrees != 45: board.rotation_degrees = 45 elif right and down and !up and !left: board.position.y = board_comb_off board.position.x = board_comb_off if board.rotation_degrees != -45: board.rotation_degrees = -45 else: board.rotation_degrees = 0 board.position = Vector2.ZERO

The item_correction function was alot of fun to work out, because it signifies my first attempt to integrate items into Mask of Undying. Essentially, if the player has a knife, the above function will simply stick the knife at the end of the Arm, no matter what direction the player is currently facing. Think of it like a knife thrust instead of a knife swipe.

At the moment, the board behaves exactly like the knife. Eventually, I'll need to fix this so that the board is swung instead. For now, through, I preferred to confirm that I could get both appearing ingame.

#move attack out of player body to deal damage func move_arm(delta): if punch: #true as soon as button is pressed. Not set to false in parent script. p_time -= 1 #timer for how long punch hangs out if right: self.position.x = offset if left: self.position.x = -offset if up: self.position.y = -offset if down: self.position.y = offset else: self.position = Vector2.ZERO if p_time <= 0: get_parent().punch = false #simply using the variable isn't enough to #modify the parent. Must re-call to parent. p_time = ori_p_time get_parent().fist.visible = false
The move_arm function does something that is probably a little silly; It reaches back into the parent script and fiddles with the variable that it's using to activate itself. I've ensured the boolean variable "punch" can only be switched on by the player, and that the Arm script has full control of when the boolean is switched off, but this still strikes me as a potential issue source if I were to share this project with anyone else.

One thing that I'm proud of is how much smaller the above function is versus some of my earlier punching scripts. To be fair, the result is more game-y looking than ever, and I haven't implemented a rotation for the actual Arm sprite yet. However, rotating and positioning the Arm was one of the first things I dealt with in my last version, so I'm not worried about it.
func _physics_process(delta): item_correction() get_parent_var() move_arm(delta)
Finally, the physics_process brings it all together. Pretty simple stuff. I'm happy with it, though.

For my next post (the 19th) I'll be covering A* pathfinding. Why? Well, after a great deal of thought, I'm convinced it's what Mask needs. Additionally, I have a working implementation. I don't have A* completely figured out, unfortunately, but I do have a basic understanding of the the Godot implementation.

Until next week!
 
 

 4/19/20:

For almost a month of weekends (and some weekdays) I've been chipping away at the problem of how to get pathfinding working in Mask of Undying. I tried the Astar2D node built into Godot, a custom A* solution, and eventually remembered Navigation2D exists and gave it shot.

I'll cover my experience with the Astar2D node briefly: I'm not a fan. The level of information in the wild at the moment only seems to be enough for experts to figure it out. It was literally easier for me to drop the Astar2D node entirely and make a custom solution than attempt to customize Astar2D to my needs.

The Astar2D node must be created in code:
onready var astar = AStar2D.new()
Each Astar2D node that is created is supposed to be tied to 1 object in a scene. At least, this Reddit post claims as much, and I can't figure out how to effectively pass a created Astar2D node, or an Astar2D map, to other scripts and objects.

It might be performant to have a dozen enemies mapping out the same terrain at the same time so long as you use the Astar2D node in Godot, but I don't even wanna go there.

With Astar2D abandoned, I moved on to referencing A* implementations in Python to see if I could convert something to Godot's GDScript. Turns out, there's 2 major issues with attempting to move between base Python and the Python-inspired GDscript for A*:
  1. Python's massive module library: Most of the Python A* scripts I found make use of 3rd-party math or graphing modules. Python modules can't be imported into GDscript by default. This muddies or adds rabbit-holes to the process of figuring out how a particular script works.
  2. Linked Lists: If you don't know what the hell a Linked List is, you're as new as I was a few weeks ago. I only know what a Linked List looks like thanks to Harvard's free CS50 course on edx.org (online learning is awesome!). I don't understand Linked Lists effectively enough to use them in Godot yet, though, so my script is an attempt to work around what is apparently a typical implementation.

For more reading on linked lists and A*, here's a Gamasutra article:
https://www.gamasutra.com/view/feature/131505/toward_more_realistic_pathfinding.php
This mostly-working implementation of A* was built when I was in the midst of getting sick of it, so I was simply importing all of my test objects into 1 script:
extends Node2D onready var tilemap = $BlueTiles onready var seeker = $Seeker onready var target = $Target
We've got a tilemap (which may be easily generated using Tilesetter or combining a few squares in GIMP or something), a "seeker" to do the pathfinding and a target for the pathfinding. Easy enough so far.

Now, the global variables:
var path = [] var g = 0 var timer = 0 var timer_max = 100
path and the timer variables should be clear enough (I'm storing a path and making a simple timer in code later on). But, what's g?

One of the hardest parts of implementing A*, for me, was understanding the formula behind it. This is the most descriptive version of the formula I've seen:
f(n) = g(n) + h(n)
n = a node, typically one near the starting position (the seeker, in this case) or a node in the path or near the path to the target position.
g = cost, so far, to reach node n.
h = estimated cost from n to goal.
f = total estimated cost from node n to goal.

h was actually the easiest to find:
func heuristic(a,b): #apparently, this is taxicab geometry/Manhattan distance #finds h(euristic) using (x1,y1) = a & (x2,y2) = b return abs(a.x - b.x) + abs(a.y - b.y)
There's another way to calculate h I think, but I saw this taxicab formula pop up the most so it's the one I went with. I don't fully understand the math.

Through trial and error, it seems that the best place to declare g is outside my other functions, so I put it with my global variables at the top of my script.

Now, here's my A* function:
func a_star(start_tile,end_tile,map=[]): var star_path = [] var tile_size = 32 var dir = [Vector2(-tile_size,0), Vector2(0,-tile_size), Vector2(tile_size,0), Vector2(0,tile_size)] g +=1 #increase g outside while loop for clarity var w_count = 0 var w_max = 50 while w_count < w_max: var lowest_check = 1000 var lowest_val = Vector2() var temp_path = {} w_count += 1 for move_dir in dir.size(): #start with the number of dir to reduce runtime for pos in map.size(): #start search through map if start_tile.distance_to(map[pos]) <= tile_size: #compares player tile to map tiles var h = heuristic(end_tile,map[pos]) var f = g+h temp_path[f] = map[pos] for opt in temp_path.keys(): if opt < lowest_check: lowest_check = opt lowest_val = temp_path[opt] if start_tile != lowest_val: start_tile = lowest_val path.push_back(lowest_val) elif w_count >= w_max: print("Pathing fail!") return star_path elif lowest_val.distance_to(end_tile) <= 64: print("Complete?") return star_path
The above script steps through a number of tiles in my tilemap from a starting position until it reaches the end position or exceeds a search-allotment variable. It took me a loooonnnngggg time, and it's still not perfect. Seriously, if you see the error, you get a digital cookie. Let's walk through it:
  • The function expects to be provided a start position, an ending position, and a map of coordinates as an array.
  • There's a path variable local to the function, called star_path, that I use to return a path to whatever calls the function.
  • A variable called tile_size (the size of the tiles in my scene) works with the variable dir to define what directions astar will search in, and within what distance away any neighbors are expected to be. Essentially, these variables work in 2 for loops to isolate which tiles, out of every tile in the whole freaking map, are neighbors to the starting position.
  • The w variables, count and max, work to ensure the while loop I've set up doesn't run forever. This is the first while loop I've actually gotten working in Godot for some reason.
  • lowest_check is set to a high value, because it's going to compared to the f I generate for every tile and will be reduced in size to help isolate the neighbors of a tile.
  • lowest_val temporarily holds the positions of the tiles with the lowest f.
  • temp_path is a Dictionary, which in Python/GDscript means you can use it to store a value, in this case the Vector2 position of a single tile, with a key, in this case f. So, I use temp_path to make a collection of the lowest f tiles. Later, I extract the tile with the very lowest f and feed it into the star_path array as part of building a path to my goal.
  • Finally, I've got conditions that run in the path is complete or if the path fails.

In order to generate the start position, ending position, and a map of coordinates as an array, I use 2 different functions. The first is my array of map coordinates:
func get_map(tile_map): #gets the pos of every single tile from a tilemap #and puts the pos into an array var tile_repac = [] for i in tile_map.get_used_cells().size(): var tiles = tile_map.map_to_world(tile_map.get_used_cells()[i]) tile_repac.append(tiles) if tile_repac.size() == tile_map.get_used_cells().size(): return tile_repac
Technically you don't need this portion, as tile_map.get_used_cells() is an array of the kind I need. However, I found it to be easier to understand what was happening with things like tile_map.get_used_cells() resolved externally to the A* function.

This next function I used to find the tiles near any given object, so that objects don't have to start directly over tiles or any of that nonsense:
func find_tile(obj_global_pos, map=[]): #files the tile closest to provided pos var tile_close var closeness = 30 #less than per-tile size var tile_cor = Vector2() for t in map.size(): tile_close = obj_global_pos.distance_to(map[t]) #checking distance to all tiles in array, within array size, for closest match. if tile_close < closeness: tile_cor = map[t] return tile_cor
Just compare the global position of an object to the coordinates supplied from a map array in a for loop. Easy peasy.

All of the above (mostly) works when run from a _ready(): function. However, if you attempt to run the above every frame of the game, it'll fail. So, I made a (bad, cause it doesn't use delta and therefore may vary by computer) timer:
func timer(delta): if timer < timer_max: timer += 1 else: timer = 0

Finally, everything is called and brought together in a process function, where I also happen to be controlling the movement of my seeker in my test scene:
func _process(delta): var speed = 50 timer(delta) if timer == timer_max-1: var map_array = get_map(tilemap) var p_pos = seeker.global_position var p_tile = find_tile(seeker.global_position, map_array) var t_pos = target.global_position var t_tile = find_tile(target.global_position, map_array) path = a_star(p_pos,t_pos,map_array) if path.size() > 1: var path_next = path[0] var direction = (path_next-seeker.position).normalized() seeker.position += direction*speed*delta if seeker.position.distance_to(path_next) < 2: path.remove(0) elif path.size() == 1: print(path) var path_next = path[0] var direction = (path_next-seeker.position).normalized() seeker.position += direction*speed*delta path.clear() else: pass
The process function above has the seeker re-find the target every time the timer hits a certain value, which is good cause I changed the target so it could move (setup outside this script).

After all that, the A* function I've made does work when the seeker and target are at angles with each other or directly connected over space. However, if the seeker and target are ever directly facing over an area without tiles, like this:

the seeker in the above image (the grey/blue square, the target is the pink/red square) will move forward, stop, and then the script will report a pathing fail. Alot of Mask's levels are going to look like this, and the behavior is reproducible even when the seeker and target are otherwise close, like this:

I have no idea how to fix this bug at the moment, and I cannot have this freezing behavior in the final game. So, having been reminded that Navigation2D exists in my research, I decided to give it a shot.

I fixed my problem with 1 fucking line of code:
path = nav_2d.get_simple_path(seeker.position,target.position)
Goddammit.

All I had to do was set my scene up like this:
Reset my main script to be this:
extends Node2D onready var tilemap = $BlueTiles onready var seeker = $Seeker onready var target = $Target onready var nav_2d = $Navigation2D var path = [] var timer = 0 var timer_max = 20 func _process(delta): var speed = 50 timer(delta) if timer == timer_max -1: path = nav_2d.get_simple_path(seeker.position,target.position) if path.size() > 1: var path_next = path[0] var direction = (path_next-seeker.position).normalized() seeker.position += direction*speed*delta if seeker.position.distance_to(path_next) < 1: path.remove(0) elif path.size() == 1: pass func timer(delta): if timer < timer_max: timer += 1 else: timer = 0
And, now I've got this:
The result isn't perfect (the seeker is clearly hugging the edge of the tilemap, and would be running into traps and the like in my game), but I'm sure there's a solution or hack out there I can use.

I learned alot from trying to implement my own A*, but I'm done experimenting with pathfinding for now. I've got a game to finish.

The 3rd of May, I'll be covering... something else! I've been focused on pathfinding for so long I'm going to have to re-orient to my project. Until then!
 
 

 5/3/20:

First, an update to my post from last week. Turns out Godot's Navigation2D pathfinding also has stupid problems. For example, here's the shortest path it generates for corners:
As you can see, the Blue square, or the one controlled by Navigation2D, has decided to leave the friggin tilemap entirely in order to get to the Pink square. There may be a way to fix this, but the answer I got from researching the problem was oh golly gee, just use A*.
Thankfully, while researching A* again, I bumped into references to "Smart Tilemap 2D for Godot": https://rebound-games.itch.io/smart-tilemap-2d

Smart Tilemap 2D isn't perfect, but it's easier to setup and get usable results from than anything else I've tried. And, the developer has stated they're open to feature requests and feedback.

Now, as you may notice from the gif above, I'm currently in-between art styles. The reasons for this are twofold: A tutorial series by a YouTuber named Heartbeast reminded me that animations can save you alot of coding time in Godot, and I realized my previous art style was a little too complex for me to make animations with my current skillset.

So, how to start over? I decided a more monochrome design would be easier to work with. Accordingly, I broke the problem down into one of contrasts:

With this image, I wanted to isolate a universal look for my game: dark characters on a light background, or light characters on a dark background. My roommate pointed out that a dark character on a light background is a little "intimidating," so I decided it might be the best look for my game. In order to confirm this was what I wanted, I made an additional color test:

I used GIMP to lighten the contrast and brightness on a free texture, and I drew a simple red version of my main character before combining both in Godot.

A lighter-colored background is interesting to me. I'm not sure if I'll stick with it long-term, but I feel like it can work.

I'm still a little iffy on the character color choice. I chose red because it apparently encourages viewing a character as brash according to some reference to color theory I saw online, and I want the player to always be thinking about moving forward. However, I also didn't like how the darker red looked against the tilemap above, and decided to go with a lighter shade. Below is the resulting sprite sheet I created in Aseprite:

Working with more monochromatic characters is definitely far easier and more enjoyable than what I had before. The first 3 frames are the idle animation: the main character blinking. The 4th frame is the only frame for attacking. The final 2 frames are the walk cycle.

A 2-frame walk cycle does look really weird. There's even more hitching in it than in Mario's 3-frame cycle:




However, I can't see a place to stick in a 3rd frame without muddling everything.

Over the next few weeks, I'm going to turn my attention away from coding somewhat so that I can start generating more art. It's my hope that additional spritework, combined with Godot's wonderful sprite and animation systems, will help me speed up production and get a basic version of my game working for design testing.

The weirdest thing about developing a videogame on your own is the amount of time you have to spend waiting to test whatever design you've come up with. It's why I interact with a boardgame designers group and still tinker with boardgame ideas. Boardgames, from a design perspective, are simply immediately rewarding.

My next blog post will be on the 17th. Until then!
 
 

 5/17/20:

I've been a mess these past few weeks for reasons personal and covid19-isolation-related. So, I'll be writing today's post on how I deal with burnout, in case it helps someone.

First, I'd like to set a starting point, so here's a definition of burnout from Psychology Today:
"Burnout is a state of emotional, mental, and often physical exhaustion brought on by prolonged or repeated stress."

It can be odd to think of passion projects as stressful, but really anything that can be called work can also be stressful. This is doubly-true when one is forced to interject work into what would otherwise be relaxation time, something any indie designer with a day job will have to do.

Stress can be managed. I've been failing to manage mine, and woke up last weekend completely unable to look at my project.

I've been in this position a few times now. I was exposed to the feeling of burnout repeatedly while working on that card car racing game awhile back, and I've developed a solution that works for me.

The trick is not to completely stop working. However, hammering your head against your main project while you're burned out is typically a waste of energy. Instead, I took an idea I was still excited by, but hadn't had time to work on before, and started setting it up.


I didn't finish a basic prototype. Finishing wasn't the point. The point was to give an outlet to a small frustration, the kind associated with focusing on 1 project at a time, while using the project as a fresh lens for my processes. There's always some small way to change how you do things for the better, and I find burnout helps my receptiveness to change. It also helps that new projects are exciting, and can thereby be re-vitalizing.

The funny thing about burnout is that it rarely changes how I actually feel about something. I still want to make games, and I still want to finish Mask of Undying. In some ways, this lack of change actually makes things worse by making me increasingly frustrated that I'm not working. A self-reinforcing loop is created where I want to work on my main project so I can finish it, while being unable to so much as open my main project, but still really want to finish my main project... It's usually far, far better to step back instead of entertaining this loop.

I spent the following weekdays not working at all. No tutorials, no programming, no after-work designing, nothing. I took care of chores and exercise, and tried not to bleed too much negativity on those closest to me.

5 weekdays isn't enough time to fully come back from burnout. I considered taking this weekend off too, and ended up doing a little sprite work anyway. Art, as a form of pure expression, is rarely stressful. Besides, I really need to get better at pixelart.


I've got a week off from my day job coming up before my next blog post, and I am hoping to complete a true basic prototype for Mask of Undying that week. The prototype will include:
3 enemies
1 boss
5 traps
2 tilesets
Health and death mechanics

If I do successfully complete all of the above, I'll finally be in a position to determine if anything about my idea is fun.

The reason I've never tested my idea on whether or not it's fun is simple: I was single-mindedly focused on finishing, and I worked on components I wanted for the final game haphazardly as a result. You'd think I'd know better than to work in this way. Some lessons must be re-learned repeatedly, I suppose.

There's a chance I may dislike my game after prototyping it. There's also a chance that I could be so excited by it that I decide to try and share it early on itch.io for feedback.

My next post will be June 7th. Until then.
 
 

 6/7/20:

Picture it: Thursday, the last week of May. I'd just finished the first version of my Mask of Undying prototype and had started playing with it. Adjusting enemy speeds, timings, and ranges, that kind of thing.

Everything was going good, so I decided to take the logical next step: make a new level based on what I had so far, and setup transitioning from my first level to my second. I set up the new level, setup a transition, and played the game.

Crash. No error, no explanation, nothing. Every time I navigated to the level transition and attempted to use it, the editor game window would close out completely. This raised a series of questions:
  1. Could it be the code I'm using to transition between levels? No, the one-liner get_tree().change_scene(next_scene) is generally considered safe.
  2. Has anyone else reported a similar issue online? Brief Google search says not really.
  3. Could it be the SmartTilemap2D add-in?
Lucky for me, I've got an earlier version of Mask that doesn't have the SmartTilemap2D add-in integrated. I loaded this earlier version and tested level transitions. get_tree().change_scene(next_scene)
worked perfectly.
After a minute or so of swearing, I sat back and considered my options. I don't know enough about C++ to debug the plugin itself. Maybe there was something I could do from within GDscript? It was clear the issue was occurring during a transition between scenes, so the problem could be related to how Godot was trying to unload the SmartTilemap2D node in my level.

I loaded a test project, and eventually succeeded on triggering a level transition if I ran the following code beforehand:
var stile = get_child(0) stile.clear() if stile.get_used_cells() == []: get_tree().change_scene("res://AStar/Scenes/Seeker.tscn")
This code, when run from the root-node of a scene (if you think of a scene as a tree, the first node is always the central root from which all other nodes in the scene extend) gets the SmartTilemap2D (the first child, or child0), clears all tiles present in scene that are attached to the SmartTilemap2D node, tests if no tiles are left, and then runs a scene transition.

Unfortunately, the above code didn't work when run from my main project. So, I started testing from within my main project.

I would usually recommend making a copy and working in the copy instead, but I was impatient.
Eventually, I successfully resolved the issue in my main project with 1 line of code before transitioning scenes:
self.remove_child(smart_tile)

I reported the above issue to the creator of SmartTilemap2D on Itch.io, and he updated the plugin a few days later. So cool, problem solved, no one else should have the same issue. New problem time!

I started messing around in my second prototype level, which was significantly larger than the original prototype level, and noticed a very odd kind of tile flickering.

By this time, I had given up on SmartTilemap2D for Mask. I had enough of a prototype to test using collisions instead of A* for enemy movement, and I found something I didn't expect: Enemies with small enough awareness zones can be fun to fight in levels filled with traps.

My initial reasoning for A* made a kind of sense: If a level is filled with traps, and enemies are only using basic pathfinding, it would be too easy for players to steer enemies into traps and avoid them all together. However, this reasoning makes the assumption that there would be enough space in a level for a player to easily maneuver, and it assumes that enemies would possess a large enough awareness to be easily steered safely.

The above gif was taken from a test before I stripped SmartTilemap2D. There's only 1 enemy present, and it moves fairly slowly (on purpose). However, I was surprised to find that steering enemies towards traps (not shown) could be tricky within the confines of a tight level. It was the small awareness field (the large blue collision circle) that seemed to make this possible, and the result was kinda fun!

I had my roommate play the test level, and he concurred that he was having fun.

So, I stripped out SmartTilemap2D from Mask and re-loaded the second test level. And, the tile-flickering re-occurred. Wtf?
 
Turns out, the flickering issue I was seeing is a known issue that is potentially related to Nvidia's videocard drivers:

I built a test project and went through the recommended solutions. I started by going to Project Settings > Rendering > Quality > and enabling "Use Pixel Snap" and "Use Nvidia Rect Flicker Workaround". No change.

Next, I tried switching my project from Godot's Gles3 renderer to the Gles2 renderer. I couldn't tell you what separates Gles3 from Gles2 technically. I did find a YouTube video that explains what difference Gles2 and Gles3 make on game-making:

Switching to Gles2 brought up a whole new issue where scenes in my project failed to load correctly:

Finally, I downloaded the beta3 version of Godot 3.2.2 from this site:
https://downloads.tuxfamily.org/godotengine/3.2.2/

The Godot beta version worked!

I spent Saturday and this morning re-building my prototype in Godot 3.2.2beta3 under the Gles2 renderer. I can now say the prototype is good enough for actual game design testing.

This feels like the first real progress I've made in a year. I don't regret the learning that happened along the way. I still plan to use SmartTilemap2D for a project someday, for example. I'm annoyed it took this long to get to a prototype.

My next post will be on the 21st. Until then!
 
 

 6/21/20:

Instancing in game engines typically relates to using code to generating a copy of an object while a program is running. This is not the best explanation for what instancing is. However, the full explanations trip and fall down confusing rabbit holes related to what a class is in scripting, and that's way more than anyone actually needs to know to read this blog post.

Say you have an enemy that needs to shoot something out from themselves that isn't bullets (bullets are usually handled differently). An enemy that hocks up a mouthful of toxic, corrosive spit and shoots it at your player. And, you need that spit to turn into a dangerous splotch on the wall when it hits.

I know, that's a very specific use case, but it's one of the instancing issues I found myself tackling these past 2 weeks, and it's the one I feel best equipped to go over.

First, let's start with the enemy, the Spitter:
extends KinematicBody2D var spit_ref = preload("res://Enemies/Basic/FlyingSpit.tscn") onready var detect = $DetectionZone onready var timer = $SpitTimer export(float) var wait_max = 3 export(float) var wait_min = 0.2 export(int) var health = 2 var wait_time = 0 var pause = true
The important variable here is spit_ref. The other variables relate to how long the Spitter waits between shots, how it notices the player is nearby, and how much health it has.

Somewhere else in my game, there is a scene I've named "FlyingSpit". My enemy, the Spitter, needs to know where the scene is and needs to preload it in order to effectively copy from it.

func _ready(): randomize() wait_time = rand_range(wait_min,wait_max) timer.wait_time = wait_time
At some point, I decided it would be interesting if each Spitter in my game shot spit after a random wait time. Randomization in Godot is kinda funny though. Every random element works off the same seed when the game starts. So, I used Godot's randomize() function to ensure each Spitter gets a different randomization seed (so each Spitter's wait time is actually random).

Unfortunately, randomizing how long Spitters wait to shoot makes approaching any given Spitter just hard enough to be annoying, so I'm planning to change this later.

func _process(delta): if detect.can_see_player() and pause == false: var dir = self.global_position.direction_to(detect.player.global_position) var spit = spit_ref.instance() spit.toward_player = dir self.call_deferred("add_child",spit) spit.global_position = Vector2(20,20) * dir timer.wait_time = wait_time timer.start() pause = true
Now, per the _process function above, the Spitter enemy doesn't move. It waits for the player to enter its range. Once the player is in range, the Spitter reaches into the "detect" variable, which is a reference to an Area2D node attached to the Spitter called "DetectionZone", and pulls out the player's real position in the game world. The Spitter then uses Godot's direction_to() function to compare the player's position to its own to determine a direction.

Now, fully equipped with the player's direction, the Spitter instances spit. Essentially, the scene that was preloaded earlier in the program is copied whole into the game. But, there's a problem: The instanced spit is also outside of the game world!

A brief aside that I'll tie back to the main point in a moment:
One of the things I like about Godot is the clarity of its structure. Each scene, and the engine itself while running, is broken down into a tree structure. I'm sure most game engines operate the same way, but Godot is the only game engine I've tried where you can view the structure while running your game.


The above screenshot is from a running prototype level. Everything listed under "Prototype" composes the level a player sees and plays: the tilemaps I used to build the level, the enemies, the player character, the traps, and so on. Above Prototype are the 2 global scripts I'm currently using, Bounce and Global. Global carries information between scenes/levels, and Bounce is referenced by objects that need to physically bounce. Last but not least is the "root" node, which is the parent of all other nodes in a running scene.

The point here is that everything should be a child under the root, and anything you actually want to use in a scene should either be a child of the level itself or a global script of some kind. Instanced objects must be parented under another object in a scene, and in the script above I've used self.call_deferred("add_child",spit) to make the spit a child of the Spitter itself. I had to use "call_deferred" for technical reasons I don't fully understand.

Making an instanced attack a child of an attacker, as I've done with the Spitter and spit, is typically not advised. If the player kills the Spitter, any spit that is currently present will be destroyed soon after. I prefer this approach for Mask, however, because the levels are tight and it feels like a reward for getting the last hit on the Spitter.

The line
spit.toward_player = dir
reaches into the newly-instanced spit and modifies a variable called "toward_player" with the previously-obtained dir variable.

spit.global_position = Vector2(20,20) * dir
sets the instanced position of the spit to be in front of the Spitter in the direction facing the player.

The remaining code works in combination with
func _on_SpitTimer_timeout(): pause = false
to force the Spitter to pause after each shot.

Now, wasn't that alot to go over? Well, now we gotta cover the spit itself!


extends Area2D var splat_ref = preload("res://Enemies/Basic/WallSplit.tscn") var toward_player = Vector2.ZERO var speed = 100 var damage = 1 var end_self = false
Looky looky, the spit has it's own preloaded scene. This is how I solved the problem of the spit going splat and goo-ing up walls: The spit itself has preloaded its own splat, here called "WallSplit" because I occasionally mistype things and fail to notice until it'd be a hassle to change. The spit also has speed, damage, and a suicide variable.

func _process(delta): if end_self == true: queue_free() if toward_player == Vector2.ZERO: queue_free() else: self.position += toward_player * delta * speed
In the _process function, we first check if the spit still needs to exist. If it does, we then check that we have a player position. If we don't, bye-bye spit. If we do, the spit speeds towards the player position the Spitter gave it.

func _on_FlyingSpit_body_entered(body): if body.name == "Player": self.queue_free() else: var splat = splat_ref.instance() var main = get_tree().current_scene main.add_child(splat) splat.global_position = self.global_position end_self = true
Now, the spit is an Area2D, so it has the ability to easily check collisions (kinematicbodies can, too, but they're not as easy or reliable). The spit needs to be able to collide with 2 things: the player, and walls. If the spit hits the player, the damage value is relayed to the player and the spit destroys itself. However, if the spit misses and hits a wall, it instances a dangerous splotch of goo at the exact spot where it struck the wall before destroying itself.

In the case of the spit, we cannot have the splat made by the spit set as a child of the spit. The splat would just disappear with the spit when the spit destroys itself. So, we can use
get_tree().current_scene
to get the parent of the entire level, Prototype. We then make the splat a child of the Prototype level, and we set the splat to have the same position as the spit before the spit vanishes.

All well and good. But, what if spit hits a spot where spit has already been? Won't this cause an issue of stacking splotches on walls until the game crashes?

Probably, so here's my solution:
func _on_FlyingSpit_area_entered(area): if "WallSplit" in area.name: self.queue_free()
The above function checks if spit has entered a pre-existing splat. If so, no new splat is created. The spit just destroys itself.

Now, I could go over the wall splat, but all I needed was 1 line:
var damage = 1

I'm in the process of adding more, but that's for later.

Feel free to let me know if there's a better way to do what I've done above. 1 condition! You have to explain yourself.

My next blog post will be on the 5th of next month. Until then!
 
 

 7/5/20:

'm afraid this post will be a little basic to most Godot users. My game's design shifted abruptly this past week to something smaller, leaner, and far more uncertain. I've found myself questioning everything up to the choice for the game's name, which I may soon change. So, I'll just be focusing on a few small and unchanging parts of my game, and I'm going to return to calling it Project Splatter until I can re-decide on the name.

Before I can get into the health pickup, let's cover a solution I learned from HeartBeast on YouTube I use as part of the pickup's foundation: the "DetectionZone".

The DetectionZone in my project is an Area2D node with an empty CollisionShape2D as a child. Essentially, the DetectionZone is a re-usable means for any object of your choice to see and react to the player. Any time you import the DetectionZone into a scene, you can right-click on it, enable Editable Children, and scale the DetectionZone's CollisionShape2D to be however large or small you need it to be just in that one scene.

As the DetectionZone only exists to see the player, I've removed it from all physics Layers and set it up on a Mask layer to detect whenever the player collides with it.

The script attached to the DetectionZone is brief:
extends Area2D var player = null func can_see_player(): return player != null func _on_DetectionZone_body_entered(body): player = body func _on_DetectionZone_body_exited(_body): player = null

Because the DetectionZone exists on a Mask layer that can only see the player, it cannot interact with any other physics body. So, it's safe to connect the Area2D that serves as the root of DetectionZone to itself using the body_entered and body_exited signals. Assuming you size the CollisionShape2D correctly, the body signals should set the "player" variable to be equal to the actual player in-game.

The function can_see_player returns a true or false depending on whether the player is within range of the CollisionShape2D you've setup. If player != null (if the variable "player" is equal to something), then can_see_player = true.

So, DetectionZone is immediately useful as a foundation for all pickups in my game because it's already setup to only interact with the player. The rest of the pickup is just a Sprite, AnimationPlayer, and Node2D, and the whole thing could probably be refined further.

The reason I have a Node2D (labeled "HealthRoot") as the root of the scene, instead of the Sprite, is because of the simple animation I made from within Godot.

As you may (or not) be able to tell, the animation for the heart pickup was made from physically manipulating the image of the heart from within the editor. To me, it now looks like the heart is being squished instead of spinning. I'm hoping new players will assume the heart is spinning. Setting the DetectionZone (represented by the blue collision square) to be a child outside of the warping Sprite avoids the problem of the DetectionZone also being squished.

The script used by the health pickup plays off the DetectionZone and could just modify the "health" value for the player. Unfortunately, my player health system has become a mess due to the need to update player health bar and move the current health value between scenes (I didn't plan it out very well).
extends Node2D onready var detect = $DetectionZone var heal = 1 func _process(delta): if detect.can_see_player(): if Global.player_health == 3: print(Global.player_health) queue_free() else: detect.player.health += heal detect.player.emit_signal("health_change", detect.player.health) Global.player_health += heal print(Global.player_health) queue_free()

The above script runs every frame, takes an amount to heal the player by (the variable "heal"), and a reference to the DetectionZone in the same scene.

If the can_see_player function from within DetectionZone returns true, the script checks a variable in a Global script (a script all other scripts can see) called "player_health". If player_health = 3, or maximum health, the pickup just destroys itself. I'm hoping to re-enforce the need for accuracy in movement choices, so a health pickup that destroys itself if the player accidentally touches it when they don't need it fits my current design.

However, if the player is low on health, the pickup increases the health value of the player (detect.player.health += heal), forces the player object to emit a signal that is tied to the health UI (detect.player.emit_signal("health_change", detect.player.health)), and updates the Global player health variable again, before deleting itself.

My health pickup is a bit of a mess. However, it's my hope that the concepts behind the pickup are basic enough to be useful for most projects.

My next post will be on the 19th. Until then!
 
 

7/19/20:

Today's post is late because of some changes I've been trying. Specifically, I've started trying to do at least 30 minutes of programming in Godot every day. So far, with covid and my day job messing up my social life, I've managed to find the time.

My increased productivity and stress has resulted in an obvious downside that I felt the full impact of Friday: simple exhaustion. It's nowhere near as bad as when I was trying to do a full hour every day, but I did find myself completely disinterested in even looking at my main project when I woke up Saturday. However, I don't feel that I'm at the point yet of needing to return to working only on the weekends. On the contrary, the exhaustion I experienced Friday feels closer to the exhaustion one gets after exercise versus what one might feel after staying up for 24 hours. It feels good.

To further support this, I followed up the crash of Friday by working 6 hours straight on my second project. Now, the work I put in I'd classify as experimental: I became obsessed with reducing the ugly mass of my character input code down to something slimmer and easier to process. To some, this might read as "I proceeded to completely waste my time". However, I now feel that I have a much greater understanding of how the input system works in Godot.

The Input call, typically used like this
Input.is_action_pressed("move_right")
is the simplest and arguably most robust input handling method. For one thing, you don't need alot of extra muss to use it. You can use shortcuts like
direction.x = Input.get_action_strength("right") - \ Input.get_action_strength("left")
to reduce the size of your movement code to a single freaking line (depending on your game).

By contrast, the (apparently) recommended input method is the _unhandled_input function:
func _unhandled_input(event): if event is InputEventKey: if event.pressed and event.scancode == KEY_ESCAPE: get_tree().quit()
_unhandled_input catches the inputs the _input (more on this one in a moment) and GUI scripts don't catch. So, hypothetically, using _unhandled_input is better because it lets you easily create and manage menus. Unfortunately, it's way easier to get behavior like this when you try to take shortcuts:

The green cube in the above image is freezing when inputs overlap. Specifically, left and right. I never figured out a solution that would allow me to combine _unhandled_input with a custom dictionary of inputs
var inputs = {"right": Vector2.RIGHT, "left": Vector2.LEFT, "up": Vector2.UP, "down": Vector2.DOWN}
However, if I used almost the same code with Input, ie
func player_input(): for imp in inputs.keys(): if Input.is_action_pressed(imp): filter_input(imp)
instead of
func _unhandled_input(event): for imp in inputs.keys(): if event.is_action_pressed(imp,true): filter_input(imp)
in combination with
func filter_input(imp): if typeof(inputs[imp]) == 5: direction = inputs[imp]
suddenly everything works!

Now, there's a 3rd method that I tried as well, the _input function
func _input(event): if event.is_action_pressed("jump"): jump()
Unfortunately, using _input resulted in the exact same behavior as _unhandled_input.

From the little research I've done, it seems the _input and _unhandled_input functions are preferred because they only react to input. The Input method, by contrast, is constantly checking with your operating system to confirm if an input has been pressed. Both _input and _unhandled_input are also far more customizable.

The Input method constantly checking if a button has been pressed explains the issue shown in the gifs. I could only get the player character to respond, even though it continues freezing, when I used
event.is_action_pressed(imp,true)
instead of the normal
event.is_action_pressed(imp)
The true flag forces the input event system to begin accepting duplicate inputs.

With the method I was attempting, the left and right inputs would occasionally overlap while I was mashing the left and right keys on my keyboard. The player would then, correctly, freeze as the left and right inputs canceled each other out. However, when I lifted my finger off of one of the keys (I was mashing, so this was all happening very quickly), the input event system (tied to _input and _unhandled_input) would still remember the overlap. If I didn't set the event system to keep duplicate inputs, it would then keep the player frozen until I tapped the key I was holding again. However, even with the event system keeping duplicate inputs, there remains a brief freeze as the event system waits for... whatever it's waiting for.

The Input method, by contrast, runs constantly, and even gives primacy to the last key pressed. So, even though I mashed keys just as much when using
Input.is_action_pressed(imp)
I was doing so fast enough that 1 key always had priority, and the player object never stopped moving.

So, if you're worried about performance, use _input and _unhandled_input for things like jumping, dodging, etc, to ensure a player's computer isn't always checking like
"Did they roll yet?"
"Did they roll yet?"
"Did they roll yet?"
...etc

Me, I'm lazy, so I might never try using _input and _unhandled_input again. Who knows.

This is not the post I wanted to write this weekend. Part of the reason I put work into my second project over my first was because I wanted to show off player state machines, I don't have a state machine setup for the player in Project Splatter, and I might never need one. Yes, I finally think I understand what a state machine is, and I feel dumb for ever feeling like I didn't. There're basically just this:
func _process(delta): match state: IDLE: idle() WALKING: walking(delta) HAMMER: hammer() LADDER: ladder() FALLING: player_input() HURT: hurt()

The problem with state machines is the logic. I've made a few for the bosses in Project Splatter, but I'm having trouble ironing out the logic for a player state machine. I'm positive I'll have it sorted before the 2nd of August, so expect my next post to be about state machines!

Fuck, time flies.
 

 8/2/20:

When I was first researching state machines, and actively trying to find the simplest examples I could, I was struck by the frequency of non-player character examples. This makes a certain kind of sense, as a basic state machine isolates possible actions and helps avoid odd behaviors (so long as your logic is sound). This is generally good for programming enemy behavior. However, this approach was not initially useful to me, as my starting point with a new game project is always the player character.

To make a long story short, I saw in my second project an opportunity to experiment with player state machines. My primary game project already has a functioning player character, and I'm hesitant to tear it apart just to implement a state machine. So, for the past few weeks, I've been working to iron out and implement the logic of 2nd project's player character using only a state machine.

I'm not happy with the state machine I've come up with yet, but it's what I've been working on so here we go:
extends Area2D #onready onready var anim = $AnimationPlayer onready var ground_check = $GroundCheck onready var ground_distance = $RayCast2D #exports export(int) var ladder_height = 32 export(int) var speed = 100 export(int) var grav_val = 600 #state enum { IDLE, WALKING, LADDER, LADDERWALKING, FALLING, HAMMER, HURT } var state = IDLE var on_ground = false var direction = Vector2.ZERO var height_multiple = 0 var dist_check = 0 signal ladder_rise(dist_check)

The variables I'm using for this character have been in flux, and are by no means complete. You'll notice this script is attached to an Area2D: For my second project, I wanted to step away from the usual Kinematic2D node type for the player. The game's design is fairly simple, doesn't require all the physics elements/movement functions of a Kinematic body, and it's the kind of thing I've been wanting to experiment with for a while.

Aside from the Area2D node type associated with this script, the main variables to notice are associated with the actual state machine:
enum {
IDLE,
WALKING,
LADDER,
LADDERWALKING,
FALLING,
HAMMER,
HURT
}
var state = IDLE

An enum is like a list, but it cannot be edited during runtime. Put another way: using an enum for your state machine ensures you cannot accidentally edit your list of possible states and break the game while it's running. You can give a name to your enum like so:
enum states{
IDLE,
WALKING,
LADDER,
LADDERWALKING,
FALLING,
HAMMER,
HURT
}
However, doing so would mean you would have to reference possible states like this
states.IDLE
instead of just calling states like this
IDLE
and I'm too lazy.
Finally, "state" is a variable that tracks what state your supposed to be in. As you can tell from the code, the player in my game starts in the IDLE state:
var state = IDLE

My next function will look very similar to the example of a state machine I posted last time. This is the section where the logic of the state machine is run via additional, appropriately named functions.
func _process(delta): match state: IDLE: idle() WALKING: walking(delta) HAMMER: hammer() LADDERWALKING: ladder_walking(delta) HURT: hurt()
You'll notice that some states are missing from the above match statement. This is because I've put them under a separate _physics_process function:
func _physics_process(delta): if state == FALLING: falling() player_gravity(delta) if state == LADDER: ladder()
I've separated my states out like this because I'm using a Raycast2D to judge the distance between the player and the ground. As a Raycast2D operates as a physics object, I need it to update every time the physics is calculated, which is a different timing from the calculations run under _process. If I don't try to update the Raycast2D when physics is calculated, weird and unwanted behavior ensues.

Now, I know there's a way to force a Raycast to update every physics frame even if it's not being called from a _physics_process function. However, there's a mess of new-to-me things I'm trying here, so I want to get the operations of the player falling and climbing ladders right before I experiment with removing the _physics_process entirely.

In this second project of mine, the player can essentially spawn a ladder or fall off the ladder anytime they want while they aren't hurt. I don't know if this feels good yet, because I haven't gotten everything working well enough to effectively test it. In order to effectively manage the state switching required, I've decided to simply respond to player input:
func ladder_check(): if Input.is_action_just_pressed("ladder_up"): state = LADDER func fall_check(): if Input.is_action_just_pressed("ladder_down"): state = FALLING
The above 2 functions kinda clutter my code, but separating them from the rest of my input code has made state management alot easier. This is my remaining player input code:

func player_input(): direction.x = Input.get_action_strength("right") - \ Input.get_action_strength("left") if Input.is_action_just_pressed("hammer"): state = HAMMER

State machines tend to require a central state that all other states return to when the player isn't doing anything. The Action RPG tutorial from Heartbeast uses a player's walking state as the central state that all others return to, reasoning that the player will almost always be moving. This is fine, but I'm looking for a little more control in my state machine. So, the central state for my machine is the IDLE state:
func idle(): player_input() ladder_check() if direction != Vector2.ZERO: state = WALKING

All this state does is check for player input. If the player tries to climb a ladder or move at all, the state instantly switches to any state that is possible from a standing position. This doesn't include FALLING, so I've effectively created a central state that shouldn't be called often once the player is playing, as my current vision for the game would be for the player to be constantly climbing ladders and falling off them as appropriate. This might be a logical problem later, but for now it's not a big deal.

My next function is related to walking around on the ground. Again, this isn't something that should happen too much. I decided to separate this from movement on the x axis while the player is on a ladder (the player can move, or hop, left and right while on ladders) because I wanted to ensure movement on the y axis, or up and down, can only happen when a ladder is present for the player to climb. At the moment I'm also changing the player's left and right speed while on a ladder, but I'm not sold on the way this feels.
func walking(delta): player_input() ladder_check() anim.play("walking") position.x += direction.x * delta * speed

The ladder_walking function is the same as regular walking save for 2 points: the ability to use the y axis to climb up and down, and the ability to fall.
func ladder_walking(delta): fall_check() ladder_check() player_input() direction.y = Input.get_action_strength("down") - \ Input.get_action_strength("up") position.y += direction.y * delta * speed position.x += direction.x * delta * speed/2
Eventually, I'll need to add a ceiling to the ladder-walking function, so the player cannot climb higher than the ladder, but I don't even have the ladder working the way I want it to yet so such concerns are postponed.

Now, before we get into the other ladder and falling related functions, I'm quickly going to go over 2 key functions.

The function for the player's hammer freezes the player, starts an animation, and then stops anything else from happening until the animation is finished. I just found out about
yield(anim,"animation_finished")
recently, and I'm a fan of how easy it is to use to force the player to wait until an animation is finished!
func hammer(): direction = Vector2.ZERO anim.play("hammer") yield(anim,"animation_finished") state = IDLE

I haven't really tested the hurt state, for when the player is damaged, but here it is anyway.
func hurt(): direction = Vector2.ZERO anim.play("hurt") yield(anim,"animation_finished") state = IDLE

Ok, now the ladder state:
func ladder(): direction = Vector2.ZERO height_multiple +=1 position.y = -ladder_height * height_multiple emit_signal("ladder_rise",height_multiple) state = LADDERWALKING
Once called, the ladder state locks out player input briefly, increases the player's height, and spawns a ladder via an emitted signal. The signal includes a variable that says how high the ladder should be. It's doesn't all work yet.

Once the player, intentionally or unintentionally, falls off a ladder, the falling state is set and the falling and gravity functions start running:
func falling(): if ground_distance.is_colliding(): var ground_global = ground_distance.get_collision_point() dist_check = self.global_position.distance_to(ground_global) height_multiple = int(dist_check/ladder_height) ladder_check()

func player_gravity(delta): if ground_check.off_ground(): position.y += grav_val * delta elif !ground_check.off_ground(): height_multiple = 0 state = IDLE
Technically, these could be part of the same function. However, I know gravity works and I'm still messing with what should happen while the player is falling, so I've kept the functions separate.

The gravity function works with a call to another Area2D node in my player scene, ground_check, to confirm if the player is touching ground. If the player is off the ground, they are sent plummeting downwards. If they are touching ground, it's back to the idle state. Easy peasy.

The falling function, by contrast, tries to update that Raycast2D I mentioned earlier to judge the distance to the ground while also checking if the player ever mashes the ladder key to try and recover from the fall.

That's it, my player state machine example. It's not all working in terms of what the player is supposed to be able to do, but the logic works. Hopefully this is helpful for someone.

I've spent the last few weeks working on this second project, and I think it's time I returned to my primary project, project splatter. My next post will be about it, and I'm hoping to've decided on a name. Until the 16th.
 
 

8/16/20:

After my last blog post, it hit me: the year is half over, and I was still adding new features to my main project (still working on a new name for it). So, I re-examined my project against the goals I set for it:
  • Make something simple.
  • Make something quickly.
  • Make everything except the game engine and audio myself.
  • Make a complete game.
  • Release it before the end of the year 2020.

I've removed the goal of necessarily charging money for the game once it's finished. The platform I'm going to release on, Itch.io, has an optional payment option that I'll probably use instead. Still, I'm not expecting to make much (if any) money.

The reasoning behind this is straightforward: I want a 30 second timer in my game, and I don't believe I'll have time to add unlocks or other incentives for repeated play.

I've wanted a timer in my game ever since I decided on the endgoal: Get to the end before your lover dies. However, for most of development, I couldn't decide on what the timer length should be. 1 minute? 5?

Until recently, I planned for up to 5 bosses to be in my game. Additionally, I'm planning on there only ever being 5 levels total: a starting area, 3 randomly selected levels from a larger pool of pre-constructed options, and a 5th final area with a bossfight. Within this structure, a 1 minute timer that stopped when you reached the final boss seemed most appropriate.

It was the 5 planned bosses, with the potential complexity they would add, which caused me to want at least 1 minute for players to experiment. However, it's on these 5 bosses that most of my time was being spent. And, only 1 was finished. So, I decided to cut every boss except for the one that was finished.

My pool of monsters and traps to learn is small, and there is no barrier in levels to keep players from rushing through everything to get to the end. Given this, with the removal of all but 1 boss I decided I wanted to focus the player on speed over doing everything. This is a cheap way to try and force interesting choices, and it's usually not a good design decision when made from desperation or a need to cut content.

I still plan to stop the in-game timer when the player gets to the end. So, I need a timer that is long enough for the player to get through 4 levels, but just short enough to force the player to move quickly. If 1 minute was necessitated by random, complex (comparatively), and unskippable boss fights, it seems reasonable to me to find the new time by cutting 1 minute in half. 30 seconds it is.

With the timer decided and all but 1 boss cut, I realized I'd implemented essentially everything my game will require. So, I now consider the game to be out of Alpha and into early Beta. My next steps are 5:
  • Create new, 1 color, simplified sprites and animations for every object in the game.
  • Create multiple tilesets.
  • Find basic sound assets.
  • Compile art, sound, and game assets into a basic playable version to beg people to try.
  • Create a side project to learn how the Godot UI layer works.

Wait, wait, what was that last one?

It's an unfortunate truth that I know very little about implementing UI (menus) in Godot. This is a big problem, as I'd like to present the player with sound and input settings at the very least. So, I've started a new project called Shoplifting Godot.

This new project is not an original design, thankfully. I'll be attempting to recreate a game from 1979 called "Shoplifting Boy" in Godot, and I've selected this game for 2 reasons: It's one of the simplest games ever made (though it was advanced for the time, I'm sure), and I've never created a decent design for a stealth game. There's plenty you can learn just from seeing Shoplifting Boy in action, but I'm curious if there's more I can learn through the remaking process.

I learned about Shoplifting Boy completely by accident thanks to YouTube recommendations:
 
My next post (9/6) is on track to be part 1 of an overview of Shoplifting Godot, and should be the release date. Assuming nothing pops up to impede my progress, of course.


9/20/20:


I've done it. In two weeks, I've taken a project that failed all of my initial goals (including a 6 month development plan) and turned it into something anyone can play in a browser:
https://dgalga.itch.io/punch-in-the-dark

The result is still not as much fun as I'd like. The way the light cone immediately changes when you move instead of smoothly rotating, the still-troublesome light cone width, and persistent animation issues, are too distracting for me to fully the enjoy the game. However, I consider Punch in the Dark to be an experimental prototype, so I can live with these issues for the time being.

My original concept for "Project Splatter" was very different, and was intended to be far closer to the game Splatterhouse in execution. However, the result wasn't very interesting, and my recent experience on Shoplifting Godot showed me I could do better. So, I decided to try limiting the player's vision, which the Godot game engine makes very easy, and I liked the result enough to name it.

The next step for Punch in the Dark will be the same as Shoplifting Godot: I need to find a way to get people to give me feedback.

I'm learning that getting a few people to play your game isn't hard. Itch.io provides wonderful free analytics, so I know that, to date:
  • 7 people have downloaded Shoplifting Godot.
  • 1 person has played the browser version of Shoplifting Godot.
  • 6 people have played Punch in the Dark.
  • 0 people have left any comments.

In terms of comments on these games outside Itch.io, I've received exactly 2 for Shoplifting Godot. This is nice, and it helped me fix some initial issues with Shoplifting Godot, but it's not enough to get a sense of whether people are enjoying my games or not. Game design thrives when you have access to players/testers who can give you detailed answers, and I'm feeling the absence. I can't tell if the disinterest is from something I'm doing, from the lackluster art of my games, from the gameplay people see when they search for my games, or something else entirely. And, at the moment, I'm not really sure how to make up for it.

I notified people that Shoplifting Godot existed by posting on Twitter, the PlayMyGame Reddit group, and Facebook to notify my friends and family. For Punch in the Dark, I managed to release the game on Saturday and tied it into the #screenshotsaturday campaign. I also posted links to it in additional Reddit groups.

It's possible there might be other sites where I could post my game and get a greater response. For example, I've been hearing positive things about Game Jolt for awhile, and I plan to post both Shoplifting Godot and Punch in the Dark there to see what happens. It's also possible there could be other Reddit groups or social platforms I can try to get some attention.

As things stand, it's clear at least a few people have enjoyed Shoplifting Godot enough to say something. As a result, I plan to develop Shoplifting Godot further into a full game starting next year.

I'm going to take the rest of this year for personal development. What does this mean? Well, I've got goals to meet at my day job, Python things to do, there's something I'm calling Project Neotokyo that I need to do alot more research for, and there's the 3rd prototype I started not too long ago I'm calling Project Wrecking Crew. Hopefully, I'll have something new and significant to report before the year is over.

I'm quite pleased with my basic menus, so next time I'll break down how I've been making them. Until 10/4!

Piecemeal Jack - post 4 - wrap up

 I'm calling it quits on Piecemeal Jack for now. This is as far as I got: I built everything I had planned, to some extent, and the resu...