Author Topic: Outpost 2 Coding 101: Week 10  (Read 11807 times)

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3238
Outpost 2 Coding 101: Week 10
« on: March 29, 2010, 04:20:27 PM »
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:

Code: [Select]
// 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:
Code: [Select]
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:
Code: [Select]
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):
Code: [Select]
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!
 
"As usual, colonist opinion is split between those who think the plague is a good idea, and those who are dying from it." - Outpost Evening Star

Outpost 2 Coding 101 Tutorials

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3238
Outpost 2 Coding 101: Week 10
« Reply #1 on: March 29, 2010, 04:22:16 PM »
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!).

Code: [Select]
// 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:
Code: [Select]
// 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?).

Code: [Select]
//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!
"As usual, colonist opinion is split between those who think the plague is a good idea, and those who are dying from it." - Outpost Evening Star

Outpost 2 Coding 101 Tutorials

Offline Flashy

  • Sr. Member
  • ****
  • Posts: 391
Outpost 2 Coding 101: Week 10
« Reply #2 on: May 20, 2010, 08:20:35 AM »
Must I put Triggers into a SaveDataStruct?
For example
Code: [Select]
CreateVictoryCondition(1, 0, CreateCountTrigger(1, 0, -1, mapEvacuationModule, mapNone, 1, cmpGreaterEqual, "NoResponseToTrigger"), "Evacuate 200 colonists to the starship.");

CreateTimeTrigger(1, 0, 1200,  4300, "Quakes");
« Last Edit: May 20, 2010, 08:20:48 AM by Flashy »
Praise the mighty light towers!!!

Offline Sirbomber

  • Hero Member
  • *****
  • Posts: 3238
Outpost 2 Coding 101: Week 10
« Reply #3 on: May 20, 2010, 09:13:02 AM »
Only if you want OP2 to save correctly.

Edit: Also, the code you posted is ugly.  This is much nicer:
Code: [Select]
// 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.");
« Last Edit: May 20, 2010, 09:15:57 AM by Sirbomber »
"As usual, colonist opinion is split between those who think the plague is a good idea, and those who are dying from it." - Outpost Evening Star

Outpost 2 Coding 101 Tutorials

Offline Flashy

  • Sr. Member
  • ****
  • Posts: 391
Outpost 2 Coding 101: Week 10
« Reply #4 on: May 20, 2010, 09:43:46 AM »
Ok.
Can I use the same trigger variable for several triggers?
Code: [Select]
SD.Trig = CreateTimeTrigger(1, 0, 1200,  4300, "Quakes"); 
SD.Trig = CreateTimeTrigger(1, 0, 5600, 12700, "Vortex");
Praise the mighty light towers!!!

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Outpost 2 Coding 101: Week 10
« Reply #5 on: May 21, 2010, 03:23:13 AM »
You only need to store a Trigger reference in the "SaveDataStruct" if you want to reference it later. By later, you can assume a different tick. So if you setup a time trigger for disasters, but never adjust the trigger after it was created, then you don't need to store a reference to it.

You can pass and return triggers from functions if you really wanted (although I'd suggest not doing that), and just store them in local variables until your code returns to Outpost 2. Once your code returns to Outpost 2, then I would say all bets are off. The entry (and exit) points for your code from Outpost 2 are basically InitProc, AIProc, and trigger callback functions. Don't try to pass values between different calls to those functions without storing the values in a GetSaveRegion protected buffer. It just might happen that between one call to your code from Outpost 2, and the next, the game was saved and reloaded, in which case all variables outside of that buffer will have been cleared. That won't happen though, as long as you have control of the CPU.


If you overwrite a trigger reference, both triggers will still exist, but you'll only have a reference to one of them. This doesn't matter if you don't need to modify them after they are created. In your example, you probably don't need to store a reference to either.

 

Offline Flashy

  • Sr. Member
  • ****
  • Posts: 391
Outpost 2 Coding 101: Week 10
« Reply #6 on: July 04, 2010, 12:30:35 PM »
Quote
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?).

Code: [Select]
//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;

}

}
I observed that combat units have to be in a group to follow the DoAttackEnemy command. The result was:

After their group is dead, they just sit there. Somewhere i found a solutionfor this: Instead of destroying the group, you could create a seperate group that is set to DoAttackEnemy. When the AI shall attack, just transfer all units with attackgroup.TakeAllUnits(waitgroup)
Praise the mighty light towers!!!

Offline Angellus Mortis

  • Full Member
  • ***
  • Posts: 138
Outpost 2 Coding 101: Week 10
« Reply #7 on: August 08, 2010, 02:24:28 AM »
I found this page by Eddy-B VERY useful for setting up Fight Groups and what to do to avoid what Flashy got.

Offline Ecke100

  • Newbie
  • *
  • Posts: 36
Outpost 2 Coding 101: Week 10
« Reply #8 on: June 04, 2011, 04:30:53 PM »
Where shell i put this

SaveData SD;

// Usage
TethysGame::CreateUnit(SD.AI_CC, mapCommandCenter, LOCATION(47+31, 98-1), SD.numAI, mapNone, 0);

I have put it under this, i get this error (member function redeclaration not allowed) what is wrong?

// OP2-specific Example
struct SaveData
{
    // Triggers
    Trigger Meteor,  // Meteor time trigger
            Vortex,  // Vortex time trigger
            CheckSecondSF;

    // AI Units
    Unit AI_CC,   // AI Command Center
         AI_SF,   // AI Structure Factory
         AI_VF,   // AI Vehicle Factory
         AI_SF2;

    // AI FightGroups
    FightGroup LynxRush, // Microwave Lynx Rush attack group
               Defenses; // Base defense group

    // Misc. Data
    int numAI;   // AI player number
};

// Usage
TethysGame::CreateUnit(SD.AI_CC, mapCommandCenter, LOCATION(47+31, 98-1), SD.numAI, mapNone, 0);

int InitProc()
« Last Edit: June 04, 2011, 04:32:45 PM by Eke100 »

Offline Flashy

  • Sr. Member
  • ****
  • Posts: 391
Outpost 2 Coding 101: Week 10
« Reply #9 on: June 04, 2011, 06:42:43 PM »
Uh, you can only call a function like CreateUnit inside of another function...
like
Code: [Select]
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
Praise the mighty light towers!!!

Offline Ecke100

  • Newbie
  • *
  • Posts: 36
Outpost 2 Coding 101: Week 10
« Reply #10 on: June 16, 2011, 12:42:32 PM »
Does anyone know how I add the buildings to SaveData for it to work

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Outpost 2 Coding 101: Week 10
« Reply #11 on: June 16, 2011, 09:59:44 PM »
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).
Code: [Select]
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).
« Last Edit: June 16, 2011, 10:01:44 PM by Hooman »

Offline Ecke100

  • Newbie
  • *
  • Posts: 36
Outpost 2 Coding 101: Week 10
« Reply #12 on: June 17, 2011, 10:35:20 AM »
// Global Script variable layout
struct ScriptGlobal
{
BuildingGroup buildGroup;
Trigger mineGroup1TimeTrigger;
Trigger mineGroup2TimeTrigger;
};

// OP2-specific Example
struct SaveData
{
   // Triggers
   Trigger Meteor,  // Meteor time trigger
           Vortex,  // Vortex time trigger
         Base2,
    CheckSecondSF;

   // AI Units
   Unit AI_CC,   // AI Command Center
        AI_SF,   // AI Structure Factory
        AI_VF,   // AI Vehicle Factory
        AI_SF2;

   // 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);

I got error C2761: 'int TethysGame::CreateUnit(Unit &,map_id,LOCATION,int,map_id,int)' : member function redeclaration not allowed.

InitProc here

What is wrong don't understand?

Offline Hidiot

  • Hero Member
  • *****
  • Posts: 1018
Outpost 2 Coding 101: Week 10
« Reply #13 on: June 17, 2011, 11:01:36 AM »
Yes, TethysGame::CreateUnit(etc.) goes inside InitProc(). To me, it seems you're using it outside of InitProc.

I'm pretty sure you can also use it in AiProc() (since it's basically an InitProc() that runs ever 4 ticks).
"Nothing from nowhere, I'm no one at all"

Offline Ecke100

  • Newbie
  • *
  • Posts: 36
Outpost 2 Coding 101: Week 10
« Reply #14 on: June 17, 2011, 03:38:38 PM »
AiProc dont work you create buldings every 4 tick :D

Offline Hidiot

  • Hero Member
  • *****
  • Posts: 1018
Outpost 2 Coding 101: Week 10
« Reply #15 on: June 17, 2011, 05:22:11 PM »
Trust me, using CreateUnit, you can overlap buildings.
"Nothing from nowhere, I'm no one at all"

Offline Hooman

  • Administrator
  • Hero Member
  • *****
  • Posts: 4955
Outpost 2 Coding 101: Week 10
« Reply #16 on: June 18, 2011, 07:01:27 PM »
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.

Code: [Select]
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.
 
« Last Edit: June 18, 2011, 07:02:37 PM by Hooman »

Offline Ecke100

  • Newbie
  • *
  • Posts: 36
Outpost 2 Coding 101: Week 10
« Reply #17 on: June 22, 2011, 09:10:38 AM »
Now its fixed ty :) , my second base work now but it creates a Structure Factory before the ConVec comes and build a Structure Factory i think need to make a tigger im not sure, im testing now :/

Offline Zhall

  • Full Member
  • ***
  • Posts: 154
Outpost 2 Coding 101: Week 10
« Reply #18 on: June 27, 2011, 07:14:39 PM »
Why dynamix.. why...   :unsure:  
Pumping out awsome.

Offline Ecke100

  • Newbie
  • *
  • Posts: 36
Outpost 2 Coding 101: Week 10
« Reply #19 on: June 28, 2011, 08:11:43 AM »
Now it works, you need to do a group by Structure Factory is complete.
« Last Edit: June 28, 2011, 08:13:42 AM by Eke100 »