https://imgur.com/M02Nn1O
This image shows the AI (in black) approaching my city (I'm Red) and about to take it. I am at the point where the gameplay is done but the AI can only play infantry, armor, and supply units - I am working on getting them to play fighter and bomber units. The AI successfully formed a defensive line with its infantry and used its armor unit to punch through my infantry unit, that retreated onto my city.
It is inspired by "Strategic Conquest," a nostalgic favorite of mine from the 1980s (I doubt its as good as I remember).
Here is the original: https://en.wikipedia.org/wiki/Strategic_Conquest
So, my graphics aren't great but its the best I could get with my budget and skills. I hired an artist that does a little work here and there for the little money I pay here and there. I think I am on my 5th user interface button change. I'm still working on replacing that and fine tuning the positioning/scale of the new ones.
My primary target is iOS, but in the short run it will be a free game on Mac on itch then I may put it on Mac App store.
I want to share with everyone what I've learned about game design patterns. Had I started out with this mindset from the beginning, I believe I would have been an order of magnitude more productive in my efforts. I started working on this about 6 years ago! But in hindsight, there was a lot of meandering that really I would call "R&D" now. I was figuring out how to do things better, and I learned by making bad decisions first.
A key design pattern I have invested more of my efforts into is to put as much of what makes the game a game into text files that are read upon launch. Think of it as a giant preference file, but as you expand on it you end up with a bunch of files. I currently have three different sets, so the game code is becoming more of a mini game engine as opposed to one game. This economizes on your time and maximizes code base re use. First game failed? Thats fine you can re use your design infrastructure on the next one.
The codebase is about 230,000 lines of Objective C/C code. Apple's environment strongly encourages you to use Swift but I started this before Swift became significant so I started with Objective C for the front end that calls a C back end. My hope someday is to make it multiplatform but I am not prioritizing that because if I can't sell 1,000 copies on iOS I highly doubt its worth porting.
What the code basically does is read the text files and configure the rules and display from what the text files say. The text files can reference folders for graphics and also sounds. So I can add graphics and sounds in a lot of things without recompiling or even opening the project.
An early design pattern I did was this:
I would make a player struct in C that contains info about the player. Like for example,
struct playerData
{
int player_id;
char *player_name;
float player_strength;
};
Pretty straight forward right? And that design pattern makes sense when you are trying to make one game.
But if you want to make a codebase that can handle different scenarios, mods, or maybe even be a full blown engine with its custom script...
You only need one generic struct to rule them all, for all your game related information.
struct genericVariableList
{
char **int_variable_names;
int *int_variables;
int count_int_variables;
char **float_variable_names;
float *float_variables;
int count_float_variables;
char **string_variable_names;
char **string_variables;
int count_string_variables;
};
Whoa, now how do you work with this?
The answer is in those text files.
You write the C code to read the text files and it parses them, and the text file will say this:
begin_node
priority 1
condition
probability (1)
end
int_var (Gold,100)
float_var (Respect,0.0)
end_node
So this is my custom script. The C code parses it, and evaluates it. There can be many nodes, and this is a behavior tree. But this is just a short example. What this is doing is with probability 100%, it is evaluating this node as true. This is a "terminal node" when this behavior tree file is called. BTW, I use behavior trees for everything. It greatly increases your ability to drastically change game behavior without recompiling.
What this node does when executed, it creates an integer value called "Gold" and adds 100 to it. By the way, whats it adding to? Well any object that calls the behavior tree! Because all objects have the same generic struct, any object in the game can call that script and get this variable added to it. It enables you to create different game objects - think a character that has different images, game attributes, name, etc - without messing with the C code. I can create a new character/whatever in text files, and then create a behavior tree for something like init or interaction, and it will call that when that happens.
It also creates a float variable called "Respect" and assigns it a 0.0.
The C code reads this action in when the node is evaluated and knows to check if the variable exists first, then sets it to that. The function call looks like this:
addIntVarGeneric(&inEntity->variable_list,var_name,value);
Because each game object gets this generic structure and the behavior trees can act on all game objects - the data of the game is divorced from the internal C code. You could search "Gold" all you want in the source code and you will never find it as a variable. But when you play the game - there it is!
A neat side benefit of this approach I could rewrite the game code in another language and the text files do not need to change.
Upon loading a scenario, the C code reads a file called "sacred.txt" that has a few hundred lines of script that is parsed to make the scenario. The file can call other .txt files in the scenario folder. The user interface can to some degree also be customized per scenario folder including adding buttons with actions specific to that scenario. So, it really has come closer to a game engine but I see it as a toolset to crank out variations on a theme. For me to consider it an engine I'd have to have more options for the map and the map graphics were like 1/2 the budget.
It was a pain to set up, but once the infrastructure is there, it cuts the time it takes to try new concepts significantly. I found I was much more willing to remove game elements that were, in hindsight, not worth it when they could also still exist in another scenario folder.
Returning back to the AI, it uses the behavior trees as well so this is a portion infantry unit AI:
BEGIN_MOVEMENT
PRIORITY 2
CONDITION
CONTACT -1 VISIBLE ARMIES VILLAGES
NOT_DUG_IN
END
DIG_IN
END_MOVEMENT
BEGIN_MOVEMENT
PRIORITY 4
CONDITION
CONTACT -1 VISIBLE ARMIES VILLAGES
NOT_NEXT_TO_ALLY
CAN_MOVE_NEXT_TO_ALLY (1)
NOT_DUG_IN
END
MOVE RETAIN
END_MOVEMENT
These are just two nodes in the tree. The C code reads all the nodes in, evaluates the CONDITION/ END block, and if true it puts it in a list of true nodes. It executes the one with the highest priority. It reads the priority as a float and multiplies it by 10000 so you can insert nodes in between by using 4.1 between 4 and 5. It doesn't matter where they are in the file, as it uses the priority value to order the true ones.
Anyways so what this does is asks if it can move next to an ally, and its not already next to an ally, it should move next to an ally. It only does this when there are enemies visible. I just noticed I set range to -1 so thats entire visible map. That should be like 8.
If that node evaluated as false then maybe the next one would be true - that one says if its next to an ally, it digs in.
Now, I should note - higher up the behavior tree it checks if it should be in defense/offense mode. It only gets to this point if its in defensive mode, which is when the nearby enemies are roughly equal or outnumber it.
Anyways this is the key part of the tree that is causing the black infantry units above my units to be dug in.
The advantage of using the trees in .txt files is I can assign a different tree to the armor unit, and be able to tweak it. Thats why that one flips to attack easier than infantry.