There are two kinds of efficiency in programming:
- Computer efficiency: use less RAM, use less CPU time, run faster
- Human efficicency: reduce develoment time, reduce difficulty of writing/reading/updating the code
Civ games traditionally sacrificed computer efficiency to gain customisability and easy rebalancing (which is cool, until you run Epic/Marathon games), but I’m ignoring that for now – it’s a bridge I’ll cross when I get to it. For now, I’ve been looking at human efficiency, since I’m the only person working on this game.
What I had
Ready to add AI, I had a very neat system of “executable player-commands” based on a cunning use of Unity co-routines. For instance, to build a city, the code that runs is:
- (Player clicks the Build button)
- Code looks at the current unit, current location, starts building a city
- Code needs input from player; game keeps running, but the Execute thread pauses
- Code waits (perhaps forever) for player to name the new city
- Player can scroll the map, look at other units, think about making a decision
- Animations, timers, etc continue running
- Message popups in multiplayer carry on working, and can be responded to
- Unless the player Cancels the city-build dialog, all other commands are blocked
- (Player … eventually … hits Cancel or OK with a name)
- Code resumes running, builds the city
- Code pre-selects the city for the player to inspect
- Code triggers a popup on player’s screen with City-Management screen
This is awesome – all that code is written in one block, and is easy to read, maintain, improve, extend. But for the AI, it’s a nightmare:
- AI can’t put input into a dialog box
- …I could hack it by adding lots of fragile code for inspecting the HUD
- …or by tightly coupling all the code, so that every action was overridable by AI
AI doesn’t want to wait!
Some actions, we do want the AI to wait – e.g. displaying movement of AI units on players screen:
- While the AI is processing its turn, player should be able to use some, but not all of the user-interface
- While the AI is processing its turn, player should get to see the commands issued, including the HUD elements
“Player should see the commands the AI issues” ?
This is one of my gripes about UX design in all versions of Civ: you get to see the results of AI behaviour, but never quite know what the AI did that caused them.
If you look at my latest progress video for this week, and watch when the AI moves its settlers, you’ll see the dotted-lines appearing on my (the player’s) HUD. I’m getting to see some of the AI HUD on my screen, so that I have increased clarity over what is happening!
This isn’t finished yet. I want you to be able to see the green movement-preview arrows that the AI issued, just like the player ones. But … it’s going to be tricky, because I only want the player to see the parts of each arrow that are outside the fog-of-war. Some convoluted programming required to split-up the visible parts like that…
Fixing it with Facades
Back to the problem: it would be a nightmare for human efficiency if I took either of the simple approaches:
- Re-write every command to have an AI version and a Human version – too much wasted coding time
- Remove the clever co-routine stuff – greatly reduces human efficiency when adding new commands and features
Instead, I identified the core parts of the HUD that are used when issuing commands, and put them into a new C# interface. This interface now has two implementations: one for humans (the one I’d been using all along), and one for AI.
How does AI do this?
Well, every time the interaction would ask the AI a question, it’s in response to a command that the AI has deliberately triggered. So … the AI’s fake/facade HUD class lets the AI say:
I’m about to trigger something interactive; here’s the answer to the question you’re about to be asked
e.g. for building a city, the source code looks like:
facade.stringThatNextQuestionWillReturn = ChooseNextCityName();
ExecuteOneCommand( commandBuildCity, currentSettler );
e.g. for moving a unit, the AI code looks like:
facade.hexThatWillNextBeSelected = targetHex;
ExecuteOneCommand( moveCommand, currentUnit );
Short, simple, easy to maintain. Just what I wanted!