Postmortem: Dash and His Ghost Girlfriend


Planning

I decided to try the Godot Wild Jam as my second game jam because it gave me more time to develop a game, and also because I've been using Godot for the past few months now. I decided that regardless of the theme, I was going to do an RPG. When the theme and wildcards were announced, I decided that I could do Through The Teeth (Lie to the player constantly) and Pursuer (Include an entity that pursues the player) by creating a passive-aggressive ghost girlfriend who follows the player around and lies about everything.

The story itself was the idea that you were someone who woke up in a Halloween corn maze with no memory and had to figure out what was going on. This was based on me having just watched The Fifth Bullet, an Episode of Castle in which a man (played by Marc Blucas - Riley in Buffy: The Vampire Slayer) loses his memory after being shot. He doesn't know if he's a killer or not. His ex-wife (played by Anne Dudek) enters the picture and decides to give him a second chance. So, I stole that story, but thought, "What if the ex has an ulterior motive and is manipulating him?"

I had already started building the game, and had decided I would do a handful of levels. Each one in an alternate reality, and each with a boss that was an alternate reality version of yourself. Killing yourself would give you a new power, like Mega Man. Like Mega Man, I was going to make level selection random and have the ghost girlfriend try to steer you to the difficult levels first. So there was going to be a zombie you, a minotaur you, an orc you, etc.

Then the final level was a version of you that looked just like you. I decided for some reason that he was going to be Dash Rendar, Vigilant of Stendar - which is a rhyming combination of Star Wars and Skyrim. Again, another lie.  I was also going to have the player input their name, and then have the girlfriend just call the player "Dash" the whole time.

I had a time, now I needed to execute.

A Tutorial

I decided that I was going to use GameDev.tv's Tutorial entitled Godot 4 C# Action Adventure: Build your own 2.5D RPG. It was 14.5 hours and would give me a chance to try out Godot in C#. (More on that later.) It felt a little cheaty, but since I did all the coding myself, I figured it's not any different from following little tutorials while building my game. Besides, it was just a starting point. I did the entire tutorial. Of the 9 days of the game jam, that took 6 of them. Partly because I lost a day to a minor surgical procedure, and then another day to a family member going to the emergency room. regardless, by Thursday night I was ready to start customizing things.

I used Google reverse image lookup to find the art that had been used for the player and enemy in the tutorial. It turned out it was sold by CraftPix. They had some other free assets and so I decided that since I didn't plan to ever use these again, I would stick to free. I had already bought Kay Lousberg's Bits Bundle with the Hallowen Pack, and I decided to use that for the first level.

Sound

The entire tutorial had NO SOUND. Nothing. Nada. So, I updated the player and enemy with sounds or all their animations. Then I made a music player, and controls for controlling the sound level. This took up my Thursday afternoon and evening. I attached optional sounds to all the menus for opening and closing, and that gave me an easy way to inject some sounds into the UI. Then I added a button sound. Oh and an Area node for playing one-shot sounds like screams.

Treasure Chests

I stayed up until 3am Thursday night working on 3D treasure chests and a treasure pumpkin. The 3D treasure chest was just porting the code from a 2D version of the tutorial chest. It took me less than an hour, including animating the chest to open and giving it squeaky hinges. It was beautiful. Then I got the great idea to use a 3D pumpkin, change the material to chocolate brown, and make a possessed pumpkin that spins and rises up, glowing and cackling at you. I spent way too much time on this - though I'm very happy with it. But when I went to bed, the pumpkin wasn't working. The next morning I woke up and fixed it. Then it was onto the . . .

Ghost Girlfriend

I decided I needed to make the ghost girlfriend next. I had to learn how to use Godot Dialogue Manager. I used Dialogic in the last game jam and found it difficult to understand quickly. I was hoping for a different experience. I also wanted to make Hannah (the girlfriend) like Elizabeth from Bioshock: Infinite. She needed to fly around randomly, inspecting things, but if the player got too far away, she needed to chase him. I inverted the enemy player detection logic, so that if the player exit's her chase area node, she chases after him.

I decided that she should be able to float up and down, but not too high or too low. I wasn't sure exactly what the area she should cover, so I decided to create an Area3D and attach a cylinder collision shape centered on her. She could move anywhere inside it, gradually going higher or lower. Then I could just change the collision shape until it worked how I wanted.

    protected Vector3 GetWanderPosition()
    {
        RandomNumberGenerator rng = new();
        NPC npcNode = (NPC)characterNode;
        CollisionShape3D wanderArea =  npcNode.WanderAreaNode.GetNode("CollisionShape3D") as CollisionShape3D;
        CylinderShape3D wanderShape = wanderArea.Shape as CylinderShape3D;
        float halfHeight = wanderShape.Height/2;
        float yPos = rng.RandfRange(-halfHeight, halfHeight);
        yPos += npcNode.GlobalPosition.Y;
        Vector2 xzCoords = GetRandomVectorInACircle(wanderShape.Radius);
        float xPos = xzCoords.X + npcNode.GlobalPosition.X;
        float zPos = xzCoords.Y + npcNode.GlobalPosition.Z;
        Vector3 globalPos = new(xPos, yPos, zPos);
        return globalPos;
    }

First I got the cylinder shape. Then I divided the height in half, and generated a random number between the negative and positive values for my Y coordinate. Then I used the radius of the cylinder to get a random vector inside the circle, and added the two together.

    private static Vector2 GetRandomVectorInACircle(float radius)
    {
        RandomNumberGenerator rng = new();
        float angle =  rng.Randf() * 2 * Mathf.Pi;
        float x = Mathf.Cos(angle) * radius;
        float y = Mathf.Sin(angle) * radius;
        Vector2 value = new(x, y);
        return value;
    }

If I'd had more time, I would've made the length of the vector random and refactored the code to reduce the number of variables, but performance wasn't really a concern. Time was.

I had her moving around randomly,. The sprites I was using for her were of a fallen angel. It was the only free female sprite I found in the same style. But I wanted her to glow, like a ghost out of Danny Phantom. During the tutorial, a flash effect had been added using a simple shader and some crazy trickery. It's not how I would have applied the shader, but it worked, and I didn't have a lot of time. So, I added in a simpler shader than adjusted them color, and then added it to the animations so it pulsed. The trick to applying it to changing images meant I had to add a second set of images for each frame for the shader to pull from. It was faster to do that manually than figure out how to do it programmatically. I was already feeling the squeeze of time. I still didn't have dialogue working.

Dialogue Manager Plugin

Going into the afternoon, it was time to start playing around with dialogue. I had to setup a talk state for the beginning of the game, and a quip state. The quip state fired off random passive-aggressive comments about the player letting her die every 3rd kill you got. I figured out how to auto advance them so the player didn't have to dismiss them. They just went away.

A problem I had, but decided to defer - was the controller couldn't interact with the dialogue. Also, you could move while the speech was going on. These were things I decided to tackle later (and never got to.) Just like the activation key hint for the treasure chests and pumpkins. That came from the tutorial. I wanted to detect the controller type and add some hints, but I ran out of time for that.

The dialogue was good enough for now. There was a little bit of story. But I really wanted more than one level.

A New Level and Gridmap Gotchas

Friday night I started building a maze level. It wasn't my first time using Gridmaps, but I was having a really weird problem. The player was falling through the floor as soon as I moved. The ghost girlfriend followed him, but that was because she had no collision set up at all. I wanted her to float in and out of things.

I spent a long time looking at collision maps and layers. Finally, I added a huge invisible static object with a giant rectangular collision shape just to stop the player from falling through the floor. But then because of the modifications I'd had to make to slop navigation to get the dungeon stairs to work, the player was able to climb over the hay bales, and directly up the maze walls.

I had been creating meshes programmatically on import, and I decided to manually add them in. I just put giant cube meshes around them and that solved the problem.  That's when I decided to do the same thing with the floor. All of a sudden, the player didn't fall through the floor anymore!

I built half the level and decided I wanted to make some zombies.

New Enemies

I'd had success making a NPC, so I figured making a second enemy shouldn't be too hard. I was right! For once, it was easy - just time consuming. I got all the sprites loaded and sounds attached for the zombie. I made them weaker than the knights as they were the first level. After I did my last export, I realized they were still too easy now that they were most of the game. Oh well. It was late, and I need sleep. I hit the hay at 11:30pm.

The Final Push - Boss Troubles

I woke up this morning at 4:30 am. I couldn't sleep, so I made coffee and got to work. Two hours later I remembered I made coffee and went back to heat it up in the microwave. I finished the maze level and created a second zombie - a fast zombie that was tougher and could chase you better. That was working great, but all of the players powers were making it too easy to beat the level. I extended the stat resource system and the reward system to turn all the special abilities on and off.  It worked great. It was still early in the morning. I wanted to create a golden treasure chest and remembered one in the dungeon assets I'd bought from Kay Lousberg. While searching for that, I realized I had a mimic. Some quick editing later (about an hour), and I had a mimic treasure chest, and a golden mimic treasure chest giving out rewards. But I had a weird bug. Setting the ability values to 1 didn't increment them, but setting them to  did. However I had a +1 Strength resource. I just copied it, made it unique and never figured out why my bug was happening. It went away.

I took a break to eat and realized I only had about 5 hours left. Yesterday I did some exporting - which was when I realized C# can't do web exports in 4.x. Ugh. So I needed to do 3 exports and upload them. I decided to give myself 2 hours. (I tried to enter a game jam before the previous one, and I ran out of time to upload because of Internet issues.) I also realized I needed to create a boss, load levels, and have dialogue. I prioritized the boss.

I thought adding a new talk state would be simple. After abandoning inheriting the enemy state class code, I just copied and pasted it and made changes. I was out of time. Sadly, the code references I had to change were many, and I couldn't get the boss to attack. I got him talking and then he just sat there. I finally got him working, and I realized I wasn't going to be able to make any more levels or figure out how to load between the two I had. I made an end level, added in 4 ability treasures, and I updated the splash screen with the wildcards I had used.  Then I exported. While I uploaded exports, I created screenshots and a picture for the page that could be used as a thumbnail. The uploads started failing.

I was freaking out. I had to close the window and reopen. I lost all my edits to the page, but I'd saved the HTML just in case. With 30 minutes to spare, I had all three version up and I submitted my game.

Thoughts on C# vs GDScript

I am noping out of C# as a programming language for Godot for now. I can always convert a GDScript project into a C# one and optimize things - but honestly if I'm needing to optimize, I'm just as likely to break out the C++. I did enjoy using multiple inheritance with interfaces, but I am preferring composition over inheritance these days anyway - especially when it comes to game code. Additionally, the Godot documentation for C# is not as easy to use. There are a number of gotchas that had me googling how to do something I knew how to do in GDScript.

I do not like editing my code in a different editor. Also, GDScript is just so well-integrated with the editor that I found myself being slowed down using C#. Exporting variables just seemed clunkier in C#, even though it's really not. Part of it might also be that I haven't used C# professionally in a number of years. I really love C# when I learned it 20 years ago. Now, I just really prefer the flexibility the GDScript offers. I would certainly go back to C# if it offered speed improvements I needed in the future, but so far that hasn't been an issue for me. I am glad I used it and got enough experience to be semi-competent in it with Godot.

I am not sad to be putting it down.

Lessons Learned

  1. Currently, I prefer GDScript to C# when using Godot. If nothing else, the fact I cannot create WebApps in C# is a deal-breaker for me.
  2. After using the two most popular Godot dialogue plugins, I haven't decided which one I like better. More research is needed.
  3. I learned some neat tricks in making state machines for characters while following the tutorial. There were a number of optimizations I wanted to make. For example, I could just make a list of all child nodes of the state machine when they are added - rather than attaching them as variables in an exported array of the state machine.
  4. I also really learned a lot about using resources. I have already started using them for music files, but when some forethought, the code is quite reusable. I didn't have to change the resource implementation at all to be able to add rewards for turning on abilities.
  5. I don't like node paths. They are certainly useful at times, but since a navigation mesh can be used without them, I find they are extra work. I'd just as soon create a spawn point and have the enemies spawn in and stay within that area - whatever it is. It would then be easy to add code for points of interest. Especially useful for NPCs - but it also allows enemies to do other things without me having to code it. If there's a chair nearby, sit down, etc.
  6. Using resources as enemy stats has a downside of not being unique. Enemies should make their stats unique when they spawn. Right-clicking and selecting Make Unique on 4 stats per enemy got repetitive fast. Plus it's easy to miss something.
  7. 2.5D RPGs are cute, but there are lots of gotchas. I had facing issues with the player, and the collisions weren't quite what I would have liked. Ultimately though, I do not like playing around with sprites for animation. It's frustrating enough in 2D. If I've already got a 3D environment, I would prefer to use 3D characters.
  8. I learned the value to creating the bare bones and then making things pretty/sound good. I managed to get a functional game, but I think a schedule would help. The last day and a half should just be for polish and playtesting/exporting. That way I have more game to play.
  9. I learned not only how to convert GDScript code (like my music controls) to C#, but I now know how to do the opposite. This gives me more tools when I can look at C# examples and convert them to GDScript.
  10. Resuse of code is super nice. I had to create everything completely from scratch because I was working in a new language. It makes me want to look at turning some of the things I use like my sound manager into a plugin.

Final Thoughts

My game fell short of my expectations. However considering I lost 3-4 days due to medical stuff, I did manage to make a functional game with a story, gameplay, rewards, and a beginning, middle and end. It far surpasses the last game I made. I'm still getting hung up in visual polish. I *like* that part of it. I would benefit from getting the next game functional and then adding the visual polish.

Get Dash and His Ghost Girlfriend

Leave a comment

Log in with itch.io to leave a comment.