Week 10
Your First Colony Game with an AI - 'Cuz Watching Your Population Grow While Doing Nothing Else Is REALLY BORING
This tutorial will be divided into two halves. In the first half we will build the colony game. In the second, we will work on AI some more.
Part I: Creating a Working Colony Game
What, you thought all you had to do was set the gametype to Colony in the DescBlock and run it! No! It's much more complicated than that!
"Nonsense! I'm only working with one human player! It's gotta be easier than multiplayer you moron!"
You forgot about saving the game.
"Oh, right... Okay, you win."
Of course I do! After all, I'm writing the tutorial, so I win be default! Now let's stop messing around and setup your colony game.
Saving The Game
Sadly, merely clicking the "Save Game" button doesn't really save everything you want to save. All the special unit handles, triggers, and other variables you want/need to keep track of are NOT saved by default, and with an AI those things are awfully important! So how do we force OP2 to save our variables?
With a Save Data struct, of course! You're probably asking what a struct is (people with programming experience outside of C/++ may know it as a "record" instead). Basically, it's a big super-variable that can hold lots of other little variables. It kinda looks like this:
// Generic Example
// Define the struct
struct structType
{
// Stuff goes here
}; // NOTE: You MUST have that semi-colon after the struct definition
// Declare the struct
structType structName;
// OP2-specific Example
struct SaveData
{
// Triggers
Trigger Meteor, // Meteor time trigger
Vortex; // Vortex time trigger
// AI Units
Unit AI_CC, // AI Command Center
AI_SF, // AI Structure Factory
AI_VF; // AI Vehicle Factory
// AI FightGroups
FightGroup LynxRush, // Microwave Lynx Rush attack group
Defenses; // Base defense group
// Misc. Data
int numAI; // AI player number
};
SaveData SD;
// Usage
TethysGame::CreateUnit(SD.AI_CC, mapCommandCenter, LOCATION(47+31, 98-1), SD.numAI, mapNone, 0);
Here's a list of things that DO NOT work:
struct SaveData
{
int numAI = 2; // Can't assign data to variables in struct definition
Unit AI_Smelter;
TethysGame::CreateUnit(AI_Smelter, ...); // Can't have code in structs
} // Forgot the semi-colon here
SaveData; // SaveData is a new variable type you've created. It needs a name, just like an int or a Trigger or a Unit. (You probably meant SaveData SD;)
SD; // Did not specify type. (You probably meant SaveData SD;)
Meteor = CreateTimeTrigger(1, 0, 1500, "SpawnMeteors"); // "Meteors" does not exist (you probably meant SD.Meteor)
A good rule of thumb is that any trigger you create should be assigned to a variable stored in your SaveData struct. Any variables your AI references (units, groups, locations, integers, etc.) should also be stored in the SaveData struct. Triggers used by mission objectives MUST be stored in the struct.
Things that DON'T need to be put in the struct include generic variables (Unit1, for example).
Go write a struct that holds a trigger for a disaster, a trigger for a mission objective, a unit handle for an AI unit, and a unit group. Write code that uses all of those variables. Come back when you're done and I'll show you how we get OP2 to actually use our struct.
You're back! That was fast. Nice work. Ready for this? This will take hours upon hours of hard work and if you mess up at all you have to start all over.
1) Go to the bottom of your code file.
2) Look for these lines of code:
void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = 0; // Pointer to a buffer that needs to be saved
bufDesc.length = 0; // sizeof(buffer)
}
3) Replace it with this (assuming you named your save data struct SD):
void __cdecl GetSaveRegions(struct BufferDesc &bufDesc)
{
bufDesc.bufferStart = &SD; // Pointer to a buffer that needs to be saved
bufDesc.length = sizeof(SD); // sizeof(buffer)
}
Whoa, that was hard! And to make it worse, we have to do that for every single colony game and/or campaign mission we write! Don't forget or your mission won't work right!
That should be all for SaveData. If you have any problems let me know. Now let's look at our AI again!
Part II: Adding an AI to your Colony Game
Really, the only thing we'll need to work with today will be Fight Groups. If you have any questions on Building or Mining Groups post in the Week 9 tutorial.
Last week I only introduced a few Fight Group functions. Let's look at all of them (the ones I understand anyways!).
// Review of Basic Fight Group Stuff
SetRect(Region); // Specifies an area for this group to wait at when idle OR ordered to defend (see DoGuardRect).
DoGuardRect(); // Orders the fight group to defend the region designated with SetRect to the bitter end.
SetTargetUnit(Unit Handle); // Specifies a unit for this group to attack or protect.
DoAttackUnit(); // Orders the group to attack the unit specified with SetTargetUnit.
DoGuardUnit(); // Orders the group to defend the unit specified with SetTargetUnit.
SetAttackType(Unit Type); // Specifies a unit type for this group to target.
DoAttackEnemy(); // Orders this group to attack the enemy, paying special attention to all units of the type specified by SetAttackType.
// Advanced Fight Group Stuff
SetTargetGroup(Unit Group); // Does the same thing as "SetTargetUnit", but for an entire unit group instead of an individual unit.
DoGuardGroup(); // Orders the group to defend the group specified with SetTargetGroup.
SetCombineFire(); // Units in the group will concentrate their fire on a single target when in combat.
ClearCombineFire(); // Units in the group will choose targets individually when in combat.
AddGuardedRect(Map Region); // Adds the specified region to the list of areas this unit group should guard.
ClearGuarderdRect(Map Region); // Removes the specified region from the list of areas this unit group should guard.
// Patrolling (Explained in detail below)
SetPatrolMode(Waypoint List); // Provides a list of waypoints for this group to patrol through. Units will fire at and chase targets that get too close.
DoPatrolOnly(); // Disables chasing behavior for a patrolling unit group. Units will still fire at enemies they encounter, though.
ClearPatrolMode(); // I haven't tried it, but I assume this either clears the group's waypoint list, or orders the group to stop patrolling.
// Misc.
DoExitMap(); // Presumably, makes the group drive to the edge of the map and vanish. But I've never tried it, so...
Notes:
-There is no DoAttackGroup() function.
-That typo in ClearGuarderdRect isn't my own. Using "ClearGuardedRect" will not work. Blame OP2's designers.
Patrolling
Before you can tell a Fight Group to patrol, you need to give it a patrol route containing a list of waypoints to visit. Here's how:
// Declare a PatrolRoute
PatrolRoute TestRoute;
LOCATION PatrolPoints[] =
{
LOCATION(42, 45),
LOCATION(53, 49),
LOCATION(68, 48),
LOCATION(-1, -1) // Important. Don't forget this.
};
// Now assign these waypoints to the PatrolRoute
TestRoute.Waypoints = PatrolPoints;
// Now, set up a fight group here
...
// Order the group to patrol using the route we established earlier
OurFightGroup.SetPatrolMode(TestRoute);
Colony Defense
Colony defense is easy. Just set up some regions and create and assign (at least) one Fight Group per region. You could also have one group defend multiple regions if you want.
I recommend you have units cover all entrances and (depending on the size of the AI base) split the base up into multiple regions, with one group stationed in each mini-region assigned to defense. You may also want to specifically defend some vulnerable structures (distant Smelters, Labs, power plants, etc.).
Attacking the Player
Attacking is a bit trickier. You need to check if the group is ready to attack and, once it is, turn off group reinforcement (otherwise you'll attack the player with a never-ending stream of units; as old units are destroyed the VFs will just pump out new ones which go directly for the player). You may also just want to destroy this group and create a new one (you don't want your AI to keep rushing Microwave Lynx at mark 3200, right?).
//Checking group size
// Global Variables
BuildingGroup vfGroup;
FightGroup OurFightGroup;
long targetSize = 10;
void AIProc()
{
if (OurFightGroup.TotalUnitCount() >= targetSize)
{
// Issue attack order
OurFightGroup.DoAttackEnemy();
// Disable reinforcement of this group
vfGroup.UnRecordVehGroup(OurFightGroup);
// Destory the old fight group
OurFightGroup.Destroy();
// Create a new one
OurFightGroup = CreateFightGroup(numAI);
OurFightGroup.SetTargCount(mapTiger, mapThorsHammer, 18);
// Update reinforcement info
vfGroup.RecordVehReinforceGroup(OurFightGroup);
// Update Target Count Data
targetSize = 18;
}
}
Alternatively, you could put this in a time trigger that fires every 15 ticks (OP2's designers opted to do this in some missions instead of using AIProc).
Growth Over Time
I'm still looking into this myself. There are a couple ways to do this, depending on what you want to do. For example, to add new buildings, just have a time trigger that goes off and adds a bunch of buildings to the recorded list. You can do the same with unit groups.
Research is tricky though. For now, just mark new techs complete for the AI after a set amount of time. It's cheating, yes, but better than what the original missions do (just give the AI access to all tech at the start of the map).
I'll post more info about this as I learn more (or somebody else can share their wisdom).
Go make a decent Colony Game!
Next Week: FINAL TUTORIAL - Your First Multiplayer Game with an AI!
Must I put Triggers into a SaveDataStruct?
For example
CreateVictoryCondition(1, 0, CreateCountTrigger(1, 0, -1, mapEvacuationModule, mapNone, 1, cmpGreaterEqual, "NoResponseToTrigger"), "Evacuate 200 colonists to the starship.");
CreateTimeTrigger(1, 0, 1200, 4300, "Quakes");
Only if you want OP2 to save correctly.
Edit: Also, the code you posted is ugly. This is much nicer:
// Assume we have a savedata struct named SD with some trigger handles in it
SD.EvacModule = CreateCountTrigger(1, 0, -1, mapEvacuationModule, mapNone, 1, cmpGreaterEqual, "NoResponseToTrigger");
CreateVictoryCondition(1, 0, SD.EvacModule, "Evacuate 200 colonists to the starship.");
Ok.
Can I use the same trigger variable for several triggers?
SD.Trig = CreateTimeTrigger(1, 0, 1200, 4300, "Quakes");
SD.Trig = CreateTimeTrigger(1, 0, 5600, 12700, "Vortex");
Uh, you can only call a function like CreateUnit inside of another function...
like
int InitProc()
{
TethysGame::CreateUnit(SD.AI_CC, mapCommandCenter, LOCATION(47+31, 98-1), SD.numAI, mapNone, 0)
// ...
}
Though i don't understand why you store the AI number in that struct, because data only has to be stored in there in singleplayer. And in singleplayer, numAI will always be 1, because the only player is number 0. Or do you plan to make it easily portable from singleplayer to multiplayer?. And I wonder if you assign a value to numAI, otherwise it would be completely useless
You may need to refine that question a little more.
Variables to hold unit references are declared in the SaveData struct, like you've already posted above. The declaration is simply a listing of what needs to go into the struct, and in what order in memory. It defines the size and layout, but does not allocate memory.
An actual instance of that struct is created by a variable declaration such as "SaveData SD;". This is what causes memory to be set aside for an instance of the struct. (In general, there may be many instances of a struct, each with it's own memory and stored values, and all with an identical layout. In this case however, it would be silly to declare more than one instance of this struct). The variable name is bound to the memory address set aside for the instance of the struct.
Outpost 2 needs to know the location of an instance of that struct. This allows Outpost 2 to write that struct to the saved game file while saving, and also to restore the previous contents when loading. It does this using GetSaveRegions. This function is passed a small descriptor for the struct, which your DLL should fill it with it's memory address and size.
Here's the general layout, using slightly different variable and struct names:
(The actualy names are of course completely arbitrary, just as long as you're consistent with them).
struct ScriptGlobal
{
// Fill in struct layout here (level specific)
};
ScriptGlobal scriptGlobal; // Declare an instance of the struct (set aside memory for it)
// Tell Outpost 2 about the instance of the struct (when it asks)
Export void __cdecl GetSaveRegions(BufferDesc& bufferDesc)
{
// Buffer for Saved Game files
bufferDesc.bufferStart = &scriptGlobal; // Buffer memory address
bufferDesc.length = sizeof(scriptGlobal); // Buffer size
}
What remains to be done, is to assign values to members of the struct instance. This depends entirely on the actual level itself, and what declarations you put in the struct. These values are often set in InitProc, or in a function called by InitProc. They are often used, and perhaps also set in trigger callbacks, or AIProc (or functions called by these).
You should give more detail on the high level objective you are trying to accomplish, what your understanding of how to translate that into code is, and what parts you don't understand, or what parts don't work, and how specifically they don't work (error messages, unexpected behavior, lack of expected behavior).
C++ doesn't allow executable statements outside of functions. You can only put declarations outside of functions. Hence, by putting the call to TethysGame::CreateUnit outside of a function, it's being mistaken for a declaration, hence the redeclaration error. Move it inside of a function, such as InitProc.
Export int InitProc()
{
TethysGame::CreateUnit(scriptGlobal.someUnitName, /* Remaining parameters ... */);
return true;
}
Also, I see you have both "struct ScriptGlobal" and "struct SaveData". As these both have the same purpose, and Outpost 2 can only save one struct, you should merge these into one. Move all the declarations into one and delete the other. Make sure the struct instantiation and the GetSaveRegions function reference the one you keep.