Phantom Forest - A Spooky Postmortem Ramble
My First "Actual Game"
I consider this to be my first "actual game". I've made many small games in the past for University/College assignments and other little things at the request of friends, but this is the first game I've made just because I wanted it to exist. I wanted it to be a game that existed as my proper introduction to game development and design, and to be something that I enjoyed making and playing.
It will forever stand as a product of many, many first-time development experiences for me. From making my first credits screen, to implementing music, to creating cutscenes, to help menus and options, and so much more.
It is something I'm proud of creating, while also respecting that it's only my introduction to game dev and is only a fraction of what I know I want to create. It will be the foundation/backbone of everything I create from now on, where what I've learned from it and the skills I've developed will exist in many little ways in my future projects.
The 4 months I've spent working on this and learning new techniques have been very demanding, but I've enjoyed them no less, despite that fact. I've learned an incredible amount in this time, and now that I'm ready to move on to my next project, I'm looking forward to continuing this long streak of learning and growing.
The idea behind learning all these new things is the desire to explore topics and techniques I've never used before. With future projects, I desire to explore new and completely different ideas, such as physics, camera work, object interactivity, button inputs, lighting, and much more.
GDScript
I decided when I first started this game to learn Godot's in-house engine script called "GDScript", which presented itself as very friendly, understandable, and easy to read. It reminded me a lot of learning Python, which has similarly friendly syntax and not too much need to worry about managing memory allocation.
Naturally, though, this is games development, so there was still a level of memory management needed when creating my class instances, but regardless, this was very simple to do, and Godot's garbage collector does a lot of the work.
Overall, I greatly enjoyed learning this language, and it was a very gentle way to introduce me to how the code interacts with the engine's "Node" system. However, the downside to learning this language is that its usage is limited to just the Godot engine, which isn't likely to be the only engine I use later on.
For my next project, I intend to try out C# to see how Godot supports that language, along with using it to improve my general programming skills.
Script Structuring
This was a great opportunity to understand how and why data and code are structured using object-oriented techniques. Turning game ideas/concepts into their own scripts for modularity and reusability was a big part of allowing this game to be expanded for as long as it was.
The original plan for Phantom Forest was much, much smaller and simpler, with no plans for animations (other than modulating the colours and alpha), no variation in stats for enemies, and only using a generalised scaling algorithm, and only one type of "battle" scene with no inheritance.
This, of course, changed as I desired to add more things I could learn from. The scripts and menus during the early stages were pretty rough and not structured as well as they could have been for later expansion and general robustness. In particular, and still impacting the cleanliness of the code to this day, was the lack of proper player and enemy scripts.
Only the battle, shop, and main menu scenes were originally planned for this game, along with a few scripts to manage them. One critical mistake I made was trying to minimise the amount of scripts to "keep it simple", but in doing so actually caused the opposite and had to read/write massive scripts for each scene. I also did the same with some of my functions and tried to make them complete entire processes in one instead of dividing them into individual use-cases. This added to problems of overcomplexity, lack of robustness and scalability, and also reduced usefulness when I wanted to perform something similar but much simpler than what that function currently did.
For example, I might want to generate 1 random piece of loot for a specific scene, however, my current loot function is designed to create all of the loot necessary for one battle instead of just creating what I need when I need it. So it makes it simple to call at the end of a battle, but it means I need to make a new function or modify the current one if I just want to generate 1 piece of loot for something else.
The solution here, from what I've considered, would be to have functions for generating individual pieces of loot randomly, and then having a bigger function for generating lots in one go that simply calls the smaller functions recursively.
Enemy Class
The enemy class wasn't originally an idea I planned to implement. It was simply going to be a few variables created in the battle scene populated using some scaling variables and dictionary data stored in the global GameManager. This changed after I felt that enemy variety and "feel" was far too similar and linear.
Because of this lack of an enemy script, there was no way to apply scaling easily to each enemy, distinguish between types, or even customise them for individual changes/events. I changed this eventually (with much frustration towards my past self) and successfully made an enemy class that supported stat changing for each enemy, but still lacked deeper customisation features.
The main features of each enemy were obtained from pre-populated dictionaries that I created and used for instantiating the enemy data and their abilities, along with all the data for my items, weapons, spells, etc. This strategy worked well enough for a game as simple as this, but for future games, I'd want to make proper inherited enemy classes with extended ability functions for each enemy.
Player Class
When it comes to the player ("The Knight"), there was originally no class for managing him with all the abilities and stats he possessed. This was the intended case originally because I only had a few different types of turn actions he could take, and I didn't feel they needed housing within their own class. How wrong I was.
At first, the game scope and idea were SMALL. Very, very small. The problem is that, being fresh to this kind of development, I had a million ideas and very little restraint, which was fine for learning a lot of first-time dev skills but was a nightmare for implementation at times.
Spells, skills, and items were all divided into "types", such as "healing", "attack", "buff", etc. The problems arose when I wanted to make abilities that weren't quite so straightforward or varied slightly from their counterparts. This meant that just handling an "attack" through a generalised function wasn't always ideal and I had to keep writing more conditions to handle that, which is generally inefficient and also an eye-sore to read and expand such code.
This, even at the end of the development, was still the case, which left me fairly limited in what new abilities I could add, but they still had an "acceptable" amount of variation despite the way they were structured, so I decided to keep it that way. There is a class I implemented for the enemies in the game called an "Action", which, had I implemented something similar for the player, it may have been much easier to manage.
In the end, I created a small player class for managing some audio emitted from The Knight's AnimatedSprite2D node. It was easier to just build a script onto the sprite and read the animation frames from there rather than externally to produce SFX at the correct times.
Dictionaries
Dictionaries were a big discovery for me when learning GDScript, and they gave me the idea for creating pre-populated databases for my items, weapons, spells, enemies, etc.
Using these databases, I was able to make a selection of "create" functions that only needed to receive an "ID" for the object that needed to be created. After receiving the ID, the function would access the relevant dictionary, collect all of the data for that object from it, and then return a new instance of that object to the caller.
It was a cheap and effective technique, but it had the disadvantage of being difficult to extend the objects manually since they each had to be written and updated 1 by 1. There was also the readability difficulty when you're trying to add more data to an object with a lot of information already in that dictionary.
I want to use dictionaries again for accessing object data, but I think I'd like to try a new technique where I create each type of object as a node containing the data within a "database" scene. The database scene would then automatically populate some global dictionaries with all of the object node data on boot-up, allowing me to both quickly access any necessary data for instantiation and also allow for easy and readable expansion. This means I can simply create some easily expandable object data scripts purely for attaching to nodes and having them read on boot-up.
One dictionary I made was used for "enemy ability" data, which stored things like "power", "duration", and "type". This was before I created the "Action" class, which I think now, with the knowledge of how I made that, I wouldn't create a database for ability data again, given that I could simply store that data within the relevant method on an enemy class script.
Globals/Autoloads
GameManager
One of the first things I wanted to consider for my game was the data/functions that will be needed throughout the entire runtime, and so my first thought was creating a general managing class called "GameManager".
I've used this to store various constants that control many aspects of the game and functions that access dictionary data to provide object instances for the current scene or global arrays. It has also allowed me to create a saving system here that can save/load data whenever a scene needs that.
There's a design choice that I've made here that I'm still undecided on whether it has been a good idea, which is storing almost all of the game's constants in this GameManager. It has been very helpful for balancing, fixing, and making general changes to most aspects of the game, all from one location, but I wonder if, long-term, that's the right way to manage a large number of constants.
AudioManager (or just "Music" in this case)
This game has been my first time managing and programming a decent amount of music, sound effects, and ambience. I needed a global node for music, that was evident quite early on. However, how was I going to manage my other audio, such as SFX and ambience?
I decided to give each main scene its own AudioPlayer for SFX, UI, and ambient sounds, depending on what each scene would need. These were fed into a few different buses so I could control each type of audio individually.
So, since then, I've come up with an idea for an AudioManager class. Despite this, I chose not to implement this class into Phantom Forest due to the large amount of time already spent and that I already had some simple ways to manage audio for the small scope of the game. This is an idea I want to use in the future, though, but for now I've settled with just the Music global and a bit of linear interpolation for fading/rising the volume of the music.
User Interface
So this is something I wish I had considered earlier in the game's creation, which is to have a global scene that manages UI elements that will be needed throughout the entire runtime, such as options and help menus.
I ended up coming up with this global scene when I decided to design my tooltip system, and I knew that was something I needed almost everywhere. After creating my tooltip system, I expanded upon that and made it into the general UI scene that it is today, but there are a few other menus in the game I wish I had created this way, too.
I also created a general-purpose confirm box with yes/no buttons in my UI scene that can be controlled with a function call. It can take a description to display in the box, and the function will return true/false depending on which button is pressed, and this is something I want to use again in my future projects.
Player (and a StateManager)
Now this is less about what I've implemented and more about what I wish I had implemented and hope to do in the future with other games. I've mentioned previously that I didn't implement a proper "player" class, but in its absence and the struggles that caused it inspired some ways I could manage that for my next project.
One thing I definitely would like to do is implement a proper state machine for my player class. I would want a StateManager to contain the player states and to easily swap them on the fly by creating different state scripts for each state to handle the methods accordingly.
If I have complex enemy designs in mind, I may also want the StateManager to be something even more general that will handle many different state types, not only used by the player.
SignalBus
In keeping with discussing some of the ideas that I wish had considered implementing for Phantom Forest, I would have liked a better way to manage some of the signals I was using, and to tidy them up just a little bit.
Signals are a fairly new concept to me, but were nevertheless used for Phantom Forest. Now that my understanding of signals has greatly improved, I'd want to make a global SignalBus script for managing signals throughout my game. This could help with easily reaching other scripts, where my design could struggle to accommodate them.
It's also something I have to do with care, ideally, since I don't want to destroy the readability of my code by having signals bounce around the place in an unclear way, or use it as a means to ignore potentially more efficient ideas. I'd want to build the game in such a way that this isn't necessary, but I want it to be considered as an option.
Canvas Layer
When I first started learning Godot and GDScript, I had to decide how I wanted to build a turn-based game, and soon learned about the canvas layer. This allowed me to create everything on this sort of UI-style layer, which was fine since this game didn't need a movable camera view.
For this style of game, it honestly had very few drawbacks and allowed me to anchor control nodes to the correct positions on-screen. However, a few objects I needed to create, such as player and enemy, needed to be something that could support sprite animations properly, and so they had to be Node2Ds instead. This gave me a little less control over them for anchoring, but this wasn't too much of an issue due to the lack of different resolution options and fixed screen position.
Overall, this method was easy to implement but undoubtedly has its limitations for anything with a bigger scope, and is likely something I won't use again in the same way I did here.
The Good
Action Class
This is my favourite idea that I've implemented into Phantom Forest. This "Action" class is used to manage actions performed by enemies in battle. It's a flexible ability-managing system that interacts with a limited number of "action slots" assigned to each enemy.
An "action" can be anything that an enemy can do on their turn, whether that means attacking, casting, or just simply being idle. When an enemy is created, they are immediately given 5 empty slots for actions, and then those slots are filled with actions created from enemy data stored within the relevant dictionary.
I felt 5 slots were enough for the scope of this game to support enemy variety, but this did not mean that every single enemy had to have 5 different abilities. The slot system was designed to support anywhere between 1 to 5 actions (or whatever the maximum is set to), and anything less than the maximum would be evenly (or as close to even as possible) copied and distributed into the leftover slots, which allowed for a great deal of flexibility.
The number of slots can easily be increased or decreased with weighted random values assigned to each, dictating the chance of any slot being selected for the enemy turn. So, using 5 slots meant that the chances of each had to be divided and weighted accordingly and I settled on dividing that into 30, 25, 20, 15, and 10%, where the first slot having the highest chance of being selected at 30% and the fifth slot being the lowest chance at 10%.
Creating the action itself was the interesting part, and I had the idea of actions just simply being the raw data for what could happen on the enemy's turn. It stored simple things like ints for player health loss and enemy healing, booleans to determine status effects, string names for animations to play and sound effects, etc. Essentially, the action class was built to support storing a large variety of data, which is then processed through the enemy turn function, which just reads the action data and acts out the turn accordingly.
The action object itself would be selected from one of the 5 slots and then processed through the enemy function for the relevant action. If the action was an "attack", then it would go through the enemy's "attack" function, which simply added data to the action object based on how an "attack" would function for that particular enemy and its stats. It would add things like the damage it will do, the sound effect it will play, and the onscreen text it will show.
The enemy's turn function would respond based on which components of the action data were changed by the enemy's function. Simply put - If there was data to read, it would add that to the turn either on the backend or frontend, depending on what it is.
If I make another game in the future where there are characters with a variable selection of abilities, I'd love to iterate on this system. It was fun to implement and solved a lot of complex problems.
Data Classes
Since the scope of this game was incredibly small when I first started building it, I had no plans for creating a data-saving system. I figured that would be something for a bigger game with more data to consider, but that quickly started to change as I came to have more and more ideas that I felt I could learn from, along with the increasing length of a playthrough of the game.
As the game grew to have a lot of different object types I needed a way of isolating and storing the data on these so that they could be re-instantiated from just their data, so I created scripts designed to hold the data of each of these object types, such as WeaponData, ItemData, ArmourData, etc. These were all Resource files that the Godot engine could easily read and write to, while also being very easy for me to read within the engine so that I could verify that the correct data was being stored.
These various data scripts were created for each object obtained in a playthrough, and then housed within a large Resource I called a "checkpoint", which was appropriate given that data saving was only something that happened when a "checkpoint" in the game was reached. This has become what is known as a "camp" in the game today.
The checkpoint was then stored at each camp and loaded whenever the main menu scene was accessed, allowing for easy continuation of a playthrough from the "continue" button, only visible when data had been successfully loaded.
This is a system I'm quite happy with, and fully intend to utilise in my next games.
Creating and Storing
With all of the items, weapons, spells and such that I wanted for my game, I needed a selection of functions that made that process as simple as possible. Therefore, I made simple "create" and "store" functions for each type of object I wanted for my game.
The "create" functions simply take an ID used to access the relevant dictionary holding the object data and then returns a new instance of the object to the caller. I felt these were created very cleanly and utilised frequently during runtime of the time, which were complemented (usually immediately) with a relevant "store" function.
The "store" functions are pretty straightforward in that they store the objects I've created within a global array but also allow for validity checking before storing. For example, to validate that storage space hasn't been exceeded or any other condition I'd like to set for that particular object type.
I love the simplicity in these functions and the ease of use. This complimentary system is one that I want to implement into future games.
Equipment System
This was a tricky system for a first-timer to implement; it took a lot of testing and double-checking that variable changes were happening correctly. However, the result was a system with straightforward equipping functions, a clear stat display, good tooltips, and rarity/quality indicators.
The tough part, in particular, was figuring out when stat changes needed to be displayed, which was far more frequently than I originally anticipated due to the equipment menu not being a global scene, which I would probably do differently for future projects.
Nevertheless, the current system works and is very satisfying to see. A lot of the fundamental functions the menu uses, I would likely use again, but they are designed more for a global scope, such that the display needs less updating.
The Bad
Lack of Inheritance
My understanding of inheritance was pretty basic before learning Godot, and as such, I didn't use it much at the beginning of developing Phantom Forest. This led to some frustration with having to duplicate the inventory scene and creating new scenes in general, along with any shared animations created in the AnimationPlayer for those scenes.
To save on any future headaches, I plan on creating base versions of scenes and objects designed for inheritance. This will help with making any changes later and expanding upon existing concepts.
Improper design of containers
Some of the oldest control objects I've created for Phantom Forest were designed in ways that I would do completely differently today. These were designed in such a way that the containers didn't hold them together properly, and this is part of the reason why it would be a lot of work to implement multiple window resolution options for the game currently.
At higher resolutions, the objects simply don't scale properly, nor do they hold together cleanly. Some of them would need to be completely re-designed to bring them up to my current interface-creating standards, which is just currently not worth the time.
The good news is that having some of these inefficient menu designs has taught me a lot about how they can be improved and why it's useful to have them designed with scalable and uncoupled techniques.
Overcramming scripts (Battle and GameManager)
When I first created the GameManager script, I wanted a lot of things in it, from the object data to almost all of the game constants, and the object create/store functions, along with the algorithms used to generate them randomly for loot rewards or shop stocking.
As much as these were easy to access, it felt like a bit of a mess to add more functions and sort through the ones I needed for different purposes. For future projects, I would divide these functions and the variables into a set of less general global scripts, such as a Player global script or an inventory/item database global script.
The battle script has a similar issue with being far too large and somewhat clunky to edit. This is another case of something that could have been improved by having a proper player class, since a lot of the functions within it would typically be classed as player-associated functions, such as buff/debuff applying, health setting, and using abilities.
It also has the issue of too many enemy-associated functions too, which could have been prevented by having the enemy class conceptualised from the beginning. Creating the enemy class later in the development did reduce the size of the battle script, but it still had quite a few leftover functions that wouldn't be easy to move without spending a lot of time redesigning the scene from the ground up.
Not building for different resolutions
This is perhaps one of my bigger issues with the game, and it's something I desperately want to avoid for the next game. I felt initially that having different resolution options and/or scalability wouldn't matter too much for a game with a very small scope, but when that scope changed over time, the resolution issue became more glaring.
One of the biggest mistakes in regards to the resolution was not building the game in a higher default resolution from the start. For my next game, I'd want to start at a higher resolution and design the game in such a way that it scales down properly, without having to worry about scaling up.
I did, however, create a last-minute fullscreen mode that functions well enough to warrant its implementation. It's not perfect, as some parts of the UI were not anchored well enough to stay exactly where I'd want them to be, but they're close enough for functional gameplay usage.
Not enough player choice
One thing I find lacking in terms of the flow of the game is player choice, particularly when it comes to dictating any sense of direction. There's player choice in the way they fight, loot, purchase, equip gear, and interact with events, but none in the sense of changing the direction of the game itself.
If I were to design a game similar to this in the future, I'd want the player to have different direction choices, such as travelling to different regions of the woods, or just other locations within that game setting. Ultimately, these different paths would lead to a similar end but it could give the player a choice over what kinds of enemies they fight, the rewards they get, the events they see, and the environments/music they experience.
Conclusion
I learned many new introductory lessons in games development while creating this project, such as interface creation, using signals, structuring large numbers of objects, using inheritance and understanding how it can save development time, creating dictionaries to store base data in a lightweight and accessible format, saving persistent data externally, understanding the importance of character scripts for code cleanliness and extendibility, and so much more.
The scope of this game was originally intended to be much smaller, but that changed after I realised there was a lot more I could learn from it. Development time ended up extending a couple of months longer than I originally anticipated, which in this case was fine because I've made something I can look back on fondly but also carry the lessons I've learned into my future projects.
I've had this feeling of wanting to start something new for a while now, and I'm relieved that I can begin work on my new idea soon, but also happy that I've gained a lot out of the time I've spent on Phantom Forest. I have so many new and different ideas I want to learn to implement into my games, and I'm very excited to start making them into a reality and learning those new skills along the way.
Files
Get Phantom Forest
Phantom Forest
Hunt down the mysterious Phantom of the Forest!
Status | Released |
Author | Ben (RNG) |
Genre | Role Playing, Adventure |
Tags | 2D, Fantasy, Ghosts, Indie, Pixel Art, Point & Click, Short, Singleplayer, Turn-based Strategy |
Languages | English |
Leave a comment
Log in with itch.io to leave a comment.