Reworking the control flow for my tactical role-playing game
Liam McGillivray
yoshi.pit.link.mario at gmail.com
Sun Mar 17 00:14:55 UTC 2024
As many of you know, I have been trying to write a tactical
role-playing game (a mix of turn-based stategy & RPG) in D. This
is the furthest I have ever gotten in making an interactive
program from the main function up. Right now, it is not yet
playable as a game, but you can interact with it and get a rough
idea of what I'm going for. Feel free to download and run it to
see what I have so far.
https://github.com/LiamM32/Open_Emblem
I'm now at a point where I have trouble figuring out the next
step to making the game playable. The complexity may have just
reached a point where I find it harder to keep track of
everything that I have written. There is probably a fair amount
of unused code that I abandoned after deciding on a different
solution, but forgot to delete. There are probably also some
amateur decisions I've made in structuring the program, given
that I largely figured it out myself.
For some time now I've thought that I may later want to overhaul
how the whole rendering and UI system work. Perhaps now is a good
time since my productivity under the current system is slowing
down.
## The Current Structure:
The code for Open Emblem (name subject to change) is split
between a source library, which handles the internal game logic,
and a graphical front-end program which uses that library, but
makes it usable.
When starting, I decided to structure it this way so that I can
experiment with different graphics and UI libraries. This may
have been a good move, even if it complicates some aspects, as
the first library I tried wasn't the one I've stuck with. I also
thought that this library may also be usable as a platform for
others to make their own tactical RPG games, though that's
unlikely with the current direction of the project.
### The Library:
The most important modules here are `map`, `tile`, & `unit`,
which contain the classes `Map`, `Tile`, & `Unit`. There is
nothing here specific to any particular graphics or game library.
Well, `Map` is now longer actually a class, as it's been replaced
by the `Map` interface and `MapTemp` template which implements
it, but for simplicity, I'll refer to `Map` as a class. This
class is meant to serve as the master that controls the flow of a
single game mission. Only one instance is meant to exist at a
time. It holds a 2-dimensional array of `Tile` objects which
represents the grid that the game is on (like a chessboard) and
an array of all `Unit` objects.
`Unit` represents a character in the game that can be moved on
the map (like a chess piece). It has some stats stored as
variables, and some functions to do various things a player (or
AI) may ask the unit to do during their turn. Each unit occupies
a tile object.
`Tile` is a square on the map, which has it's own *x* & *y*
coordinate.
The `Faction` class currently only serves to store a set of units
belonging to a certain player or AI, but is planned to play a
bigger role later.
### The Raylib Front-end:
After looking at many libraries and taking a shot at
[ae](https://github.com/CyberShadow/ae) &
[godot-D](https://github.com/godot-d/godot-d) but not really
figuring it out, I was recommended
[raylib-d](https://github.com/schveiguy/raylib-d), a binding for
[raylib](https://www.raylib.com/) by @Steven Schveighoffer.
Raylib is a rather simple graphical library written in C. I ended
up sticking with it because the website has so many
easy-to-follow examples that make it easy as my first graphical
library. They're written in, but I adapted them to D rather
easily. Of course, being written in C has limitations as it isn't
object-oriented.
This is front-end is in the
[`oe-raylib/`](https://github.com/LiamM32/Open_Emblem/tree/master/oe-raylib) directory.
For this front-end, I've made the classes `VisibleTile` and
`VisibleUnit`, which inherit `Tile` & `Unit`, but add sprite data
and other graphics-related functionality.
I then have the `Mission` class which inherits the `MapTemp`
class. This class dominates the program flow in it's current
state. It handles loading data from JSON files, switching between
different game phases and does most of the function calls related
to rendering and input.
The way I have it currently, there's a `startPreparation`
function and `playerTurn` function, each of which run a
once-per-frame loop that renders all the necessary objects and
takes user input. They each have a rather messy set of
if-statements for the UI system. Any UI elements that may pop-up
are declared before the loop begins, with if-statements to
determine whether they should be visible this frame.
For UI elements, I currently have `version` flags for either
`customgui` (which I started writing before discovering raygui)
and `customgui`, which you can select between using `dub
--config=`. Having both makes the code messier, but I haven't yet
decided on which I prefer. They are both currently achieve
equivalent functionality.
Everything here is single-threaded. Despite that, I still get
thousands of frames-per-second when disabling the cap on
framerate.
To get a glimpse of a flaw with the current approach (which may
be simpler to fix with an overhaul), try asking one of the units
to move during your turn, but then try moving the other unit
while the first one hasn't reached their destination. The first
unit will stop.
## Should I rework things?
So now I am thinking of reworking the rendering system, but also
changing some of my approach to how the Open Emblem library works.
I've been thinking of adopting an event-driven approach, using
signals and slots, for both the library and the front-end (and
between the two). I'm curious if more experienced programmers
think this is the right approach.
Play Fire Emblem. When you command one of your units to move and
attack an enemy unit, you don't just see them teleported to their
destination and the enemy dead (or lower in HP) as soon as next
frame. Instead, it will start an animation of your unit
attempting to attack the other, and after ~3 seconds you find out
whether they hit or missed (which is based on probability). In
contrast, under my current approach where a game event happens by
calling a function, everything will happen instantly. One way to
solve this would be to have the rendering object not look
directly at the underlying variables, but some cached variables
that get updated less quickly. In Fire Emblem, it's likely that
the game has already determined whether an attack succeeds or
fails immediately after it's selected, even if the player has to
wait 3 seconds before being shown. This is a little bit like how
my `Unit` objects have a variable for their grid location which
gets changed by the `move` function, but then there's another
variable to represent screen location, which gets updated more
slowly as they walk across the screen. The other option is to
have these functions happen in a separate thread, with parts
where they must wait for a signal to continue further.
Another use of signals and slots is that I can use
multi-threading for things that happen once-per-frame. When I
added the feature to make units slowly move to the destination
selected by the player, I thought I would use a separate thread,
but then I realized it would need to be synchronized with the
frames, which happens in the main thread.
If I redo the rendering and UI system, I will probably start
using [`Fluid`](https://git.samerion.com/Samerion/Fluid), which
is a Raylib-based UI system written in D.
As for the rendering loop, how should that work? I don't know how
it works in other 2D games. Should it be much like the current
approach, with a loop for every game phase containing everything
it might need to render during that phase, and using logical
statements for things that only *sometimes* appear? As an
alternative, I was thinking of making a `Renderer` object that
runs the rendering loop in it's own thread, and it has variables
to keep track of what's currently visible. Another thread would
access functions of this object to change what must be rendered.
I don't know what's the best approach.
To anyone who made it this far, thank you very much for reading
all of this. Is my current approach to rendering bad, or actually
not that far off? Would signals and slots be a good thing to
adopt?
More information about the Digitalmars-d-learn
mailing list