Jump to content
XCOMUFO & Xenocide

Iteration 2 - Interception


guyver6

Recommended Posts

Goals:

* Player is able to launch an Interceptor from Base
* UFO is randomly spawned over the Globe
* Player has one Base randomly placed on the Globe
* There are 2 Interceptors in Base
* UFO move straight to the randomly choosen destination, then is destroyed
* Only one UFO on the globe
* Interceptor is flying straight to the targetted UFO (automatically targeted)
* After Interceptor catches UFO, UFO is destroyed and Interceptor takes the Return To Base course (goes back to Base)
* Player launches first available Interceptor clicking on the Launch button on the UI
* Player is unable to launch an Interceptor when there's no UFO on Globe visible
(*) Player can rotate and zoom the Globe
(*) The Sun rotates around the Globe
* Game time handling

 

So, summarizing:

- no listing dialogs

- no base choosing

- no crafts battle

- no targetting

 

Feel free to comment.

 

EDIT: I've added 2 tasks (*) that are left from previous, unofficial iteration. The Game time is another task that we haven't had done yet, and it's required for Sun to work.

Edited by guyver6
Link to comment
Share on other sites

Plan Accepted. May I add a dependencies package made and Stackless takeover on trunk? ;)

Greetings

Red Knight

That'll be done after this iteration is done.

 

EDIT: Think this as the final demo what stackless branch can do before you accept it to be sufficent to takeover the trunk =)

 

 

- Garo

Edited by Garo
Link to comment
Share on other sites

Garo reads my mind :P.

 

I'll merge it to trunk after Iter 2, with dependencies package of course.

So, just a short question... if it "goes trunk" will you include support for VS2003? 'cause I went nuts until I finally could compile Stackless (on VS2005EE). (Though this was partly because of the dependencies...)

Link to comment
Share on other sites

First comment I’d like to make is that there is an obvious “sub iteration” or iteration 2.1. Which consists of:

1. Create UFO at random position on globe.

2. Pick random destination.

3. UFO proceeds to destination, by shortest route.

4. UFO is destroyed when reaches destination.

5. Repeat from step 1.

6. Alternately, when UFO reaches destination, repeat from step 2.

 

An additional thought, here’s a possible design for the craft class, where the craft has a strategy object that controls the behaviour, depending on what the craft is expected to be doing.

At current time we have the following missions:

1. Travel to a destination.

2. Remain idle in a base.

3. Travel towards a UFO.

4. Return to home base.

 

And later we may have the following

1. Travel to mission site

2. Search area

3. Patrol.

 

class Craft
{
public:
   setOrders(Orders* orders);
   updateProgress(int timeSinceLastUpdate) { orders->UpdateProgress(time) };

private:
   Geoposition currentPosition;
   Orders*      orders;
};

class Orders
{
   virtual updateProgress(int timeSinceLastUpdate);
   virtual checkForEvents();

   Craft& craft;
};

class TravelToGeoposition : public Orders
{
   Geoposition target;

   updateProgress(int timeSinceLastUpdate)
   { 
       craft.currentPosition += (target – currentPosition) * time * speed
   };

   checkForEvents() { if (craft.currentPosition == target) delete(craft); };
};


class InterceptCraft : public Orders
{
   Craft& target;

   updateProgress(int timeSinceLastUpdate)
   { 
       Craft.currentPosition += (target.currentPosition – currentPosition)* time/speed)
   };

   checkForEvents() 
   { 
       if (craft.currentPosition == target) { craft.fight(target); };
       if (noLongerExists(target)) { craft.setOrders(returnToBase) };
   }
}
etc.

 

Which brings me to stupid questions time. Specifically, “how does game time work in the geoscape?”

 

To explain, I would have thought that time worked something like this.

We have a real time event that fires at regular intervals (say every second.) We then convert this real time into a game time (depending on how fast the user wants game time to run.) E.g. slow time it would be 5 seconds of “game time”. We then go around each item in the planetscape that will change over time, telling each item how long it’s been since the last update. The items update themselves, and if some event of significance has occurred, it goes into an “event queue” of some sort. (To bring it to the attention of either the player or the Overmind.)

 

Side notes: A mechanism for objects which need to receive “ticks” to register themselves with each update cycle.

Also, the mechanism can evaluate how long it takes an update cycle to actually run, and adjust the frequency of the real time event to avoid saturating the CPU.

 

Now I have heard people say that this is not going to happen because we’re using stackless python, but I don’t understand how it’s going to work. There was also the claim that it will be simpler modelling the giving objects in the geoscape as state machines. (Which I don’t understand, the state models for the items in the geoscape are actually very simple, below is some of my reasoning. If someone could explain where I’m wrong, or point me in the direction of the appropriate links, it would be greatly appreciated.)

 

Activities on the Geoscape, that occur between “clock ticks”.

Note, I assume that user activities (like creating a base, giving a craft orders, fighting a battlescape battle, etc. occur instantaneously in terms of “Geoscape time”.)

1. UFO is created.

2. Human craft is launched.

3. Base is created.

4. Craft (UFO or human) moves on course.

5. Progress is made (building something in a workshop, a research project, a base facility, soldier undergoes training, soldier undergoes healing, purchase delivery, repair/rearm/refuel of craft.)

6. Progress on an item in 5 is finished.

7. UFO is sighted by a human radar (base or craft.) or vice versa.

 

 

Events that need to go into some sort of queue (when they occur during a “clock tick”)

1. Events 6 and 7 above.

2. A mission becomes available/Starts.

3. End of month review.

4. UFO enters combat range of human base/craft.

 

 

Edit: renamed class "Mission" to "Orders"

Edited by dteviot
Link to comment
Share on other sites

Every Game Frame would need to be something like this:

 

foreach ( Frame f )
{
    Translate to game time
    
    Do Geoscape Non Realtime Processing

    If realtime tick
    {
         Translate to realtime tick.          
         Do Geoscape Realtime processing
    }
}

 

The realtime processing do not need to use complete tick, I would use floating points for that... and the translation has to be performed internally; create a class that can provide Time access both in Raw and Scaled Mode (different methods for each one)... (Easy to test too ;) )

 

Greetings

Red Knight

Link to comment
Share on other sites

OK, I’ve just read “Multithreaded Game Scripting with Stackless Python”, and I still don’t see how it provides us any benefit in the Geoscape. To explain, let’s take the example of

1. Create a UFO at position on the globe.

2. Have UFO travel to another position on the globe.

 

Conceptually, this isn’t that different from the example given in the document where we have a door close.

def close(self):
   self.setVelocity(self.mMoveVelocity)
   self.sleep(self.mCloseMoveTime)
   self.setVelocity(CVector3.ZERO)

 

So code would look something like:

def craftTravel(self, startpos, endpos, velocity):
   self.setPosition(startPos)
   self.setVelocity(velocity)
   self.sleep((endpos – startpos) / velocity)
   self.setPosition(endpos)
   self.setVelocity(0)

 

But I can’t see how this would work. What is going to happen is the UFO will be at the start position, then after a period of time it will suddenly appear at the end position. Not what we want.

 

What we need to do is start with the UFO at a position, and then at each tick of the real time clock, calculate the game time, then figure out where the UFO would be, and move the UFO to the updated position. We need a function that is very similar to the update function given in the document:

void update(t_actor *actor,double timedelta){
   if(actor->state == CLOSING){
       Cvector3 pos = actor->origin;
       pos = pos + actor->velocity * timedelta;
       if(pos == restpos){
           actor->state = CLOSED;
           return;
       }
       actor->origin = pos;
   }
}

 

Note: This code is 1. wrong, 2. doesn’t show the whole story, and 3. Is more complicated than necessary for what it does. But it gives you the idea. Every tick we need to update the items position.

Now, as to why I think we should implement craft BEHAVIOUR as a state machine, is because it IS a state machine. A craft will do something, until an event happens that causes it to change. And when we look at the events, I think they’re all going to be detected by the craft itself.

 

The basic idea is we have a class for each Behaviour (state), and the class has two members.

1. update()

2. checkForEvents()

 

 

For example, lets assume we’ve got a craft with orders to patrol for UFOs. So update function looks like:

Update(timedelta ticks)
{
Position += (waypoint – currentPos) * (distance per tick) * ticks
Fuel -= (consumption rate) * ticks
}

 

However, at the end (or start) of the update several “events” could occur.

1. UFO enters radar range of the craft.

2. Alien Base is detected.

3. Craft reaches the waypoint.

4. Craft reaches point of no return for fuel. (Only just enough fuel to return to base.)

5. Craft runs out of fuel.

6. Player gives a new order to the craft. (Note, this isn’t really a problem, as we assume user input occurs outside the main loop. I.e. game time is suspended while processing User input.) But we handle this by changing the craft’s Behaviour based on the order the user has given the craft.

 

Depending on each of these events, we need to change behaviour. (In nearly all cases we change our target destination.)

 

Let’s assume we ran low on fuel, then we’ve got a returning to base behaviour, which is very similar to the previous one, except we have another “event” to check for, craft arrives at base. (Now say it with me: “inheritance”)

 

Note, the update behaviour when in a base is very different

Update(timedelta ticks)
{
if (fuel < maxFuel) Fuel += (refuel rate) * ticks;
if (ammo < maxAmmo) Ammo += (rearm rate) * ticks;
if (hitPoints < maxHitPoints) hitPoints += (reapair rate) * ticks;
}

(Obviously, it’s a bit more complicated, because we have to check that base has supplies, but you get the idea.

 

Other events that we may need to check for (in other behaviours) are:

7. We’re in combat range of a UFO.

8. Craft is badly damaged. (Would only occur after combat.)

9. Craft is out of ammo. (Would only occur after combat.)

Link to comment
Share on other sites

So code would look something like:

def craftTravel(self, startpos, endpos, velocity):
   self.setPosition(startPos) # point 1
   self.setTargetPosition(endpos) # point 2
   self.setVelocity(velocity)  # point 3
   self.sleep((endpos – startpos) / velocity) # point 4
   self.setPosition(endpos) # point 5
   self.setVelocity(0) # point 6

But I can’t see how this would work. What is going to happen is the UFO will be at the start position, then after a period of time it will suddenly appear at the end position.  Not what we want.

 

My idea is that the ufo behaviour / AI is a tasklet (a program, let's call this script the pilot) which gives orders to another layer which is more closely the core (lower level, let's call this the Craft or the autopilot), so the code above would work just like you described in the following way:

 

1) Initialize ourself: set our current position

2) Set the target position of the craft. This will internaly calculate the direction where the ufo will fly.

3) Set velocity which the ufo will fly towards the target.

4) Sleep until we should be at the target. This will block for a while. but: the engine under this will be ticked every frame and move the craft according to the target and velocity.

5) As the velocity and sleep was an assumption (but quite accurate), we cheat a bit and fix the ufo current position to be exactly where we wanted it to be at the first place

6) stop the craft movement

 

So the idea is that this script is a pilot which gives commands to the autopilot. The autopilot itself is at lower level, where a tick function is called every frame to update craft position according to "autopilot", which the pilot (the script above) has set.

 

Now let's add fuel consuption. The autopilot will watch the fuel status and sound an alarm when there is just enough fuel to return to the base. This is modelled with an exception which comes from the autopilot. I also changed a bit the script to show some features which we can implement

 

def craftTravel(self, startpos, endpos, velocity):
   self.setPosition(startPos)
   try:
     self.setTargetPosition(endpos)
     self.setVelocity(velocity)  
     self.sleepUntilTargetReached()
     self.setVelocity(0) # point 5
   except FuelLowException:
     self.setTargetPosition(homeBase)
     self.setVelocity(max_speed)
     self.sleepUntilTargetReached()

 

Here the sleepUntilTargetReached() will let the Pilot sleep until Autopilot tells him that the craft has reached the target which was programmed to the autopilot. Incase the autopilot notices that fuel is low, it interrupts the pilot, which will immediatelly turn the craft back to base.

 

If the craft would run out of fuel, the autopilot would send FuelEndedException, which would run out of the Pilot script scope, back to the lower level of code, resulting immediate craft destruction.

 

So the key in my design is that the craft has an autopilot. The script (the Pilot) gives orders to the autopilot, which implements them as accurate as it can. The pilot can go to sleep to wait until the autopilot has done its job or the autopilot can interrupt the pilot to notify that something has happend.

The autopilot has a ticker which is called every frame. The pilot is a script which executes in it's own scope/thread/virtual machine/tasklet/process (whatever you like to call it)

 

What do you think?

 

- Garo

Link to comment
Share on other sites

Garo's solution is simple and makes sense, because you have an state machine on the underground and a single tasklet that performs the scripting on the top. Furthermore, I wouldnt add too much model logic on the scripting, the script should be the driver, not the engine.

 

Greetings

Red Knight

Link to comment
Share on other sites

Garo's solution is simple and makes sense, because you have an state machine on the underground

It's even a bit exaggerating to call the autopilot a state machine. Here's an example what it might look:

 

def autopilot_tick(self, timeElapsed):
 if (self.velocity > 0):
   self.position = self.velocity * self.direction

 distanceToHome = calculateDistance(self.position, self.homebase)
 if (self.fuel <= distanceToHome * fuelConsuption):
   fireExceptionOnce(FuelLowException)

 if (self.fuel == 0):
   fireExceptionOnce(OutOfFuelException)

 

The only state is within fireExceptionOnce, which will fire an exception throught a channel mechanism back to the Pilot script and assuring that the event is fired only once.

 

- Garo

Link to comment
Share on other sites

What do you think?

 

Decoupling the "strategy script" from the "game engine" is a good idea.

However, the craftTravel script you give above is a simple case.

I'm thinking:

1. A full implementaion is going to have to respond to a number of events.

2. You will probably want to create a number of these scripts, e.g. craftPatrol, craftIntercept, etc. Then get the craft to swap the script it's currently running depending on what it's current "mission objectives are". e.g.

def craftTravel(self, startpos, endpos, velocity):
   try:
     self.setTargetPosition(endpos)
     self.setVelocity(velocity)  
     self.sleepUntilTargetReached()
     self.setVelocity(0) # point 5
   except FuelLowException:
     self.setOrders(craftReturnToBase)


def craftReturnToBase(self):
   try:
     self.setTargetPosition(self.homeBase)
     self.setVelocity(self.maxVelocity)  
     self.sleepUntilTargetReached()
     self.setOrders(craftInBase)
   except ...

Edited by dteviot
Link to comment
Share on other sites

What do you think?

 

Decoupling the "strategy script" from the "game engine" is a good idea.

However, the craftTravel script you give above is a simple case.

I'm thinking:

1. A full implementaion is going to have to respond to a number of events.

2. You will probably want to create a number of these scripts, e.g. craftPatrol, craftIntercept, etc. Then get the craft to swap the script it's currently running depending on what it's current "mission objectives are". e.g.

 

I think we should focus on the Iteration 2 goals, not on things we don't need atm. Always think of the YAGNI rule - You Aren't Gonna Need It.

Link to comment
Share on other sites

I think we should focus on the Iteration 2 goals, not on things we don't need atm. Always think of the YAGNI rule - You Aren't Gonna Need It.

 

I agree that the Iteration 2 goal is the primary focus.

However, we still need to consider where we are going, otherwise we are more prone to walking into dead ends. One of the problems of XP, with it's focus on the immediate goal. While working on the imediate goal, you still need to keep in mind what you are ultimately trying to achive. The way I've heard it put is:

While fighting the aligators, remember that your objective is to drain the swamp.

 

And on that note:

The lower level “game engine” might need to be a state machine. Consider the difference between having a base and a UFO as a target. The UFO moves (and potentially changes course) the base doesn’t. This means that the update function for a base is:

position += velocity * ticks,

 

For a UFO, we need to calculate a vector each update cycle.

(Note, simple workaround is give both bases and craft a getGeoposition() function, so update becomes

vector = normalize(target.getGeoPosition() - self.getGeoPosition())

position += vector * velocity * ticks

 

However, this means that the craft will always be heading towards the current position of the UFO, which is not a minimum time to intercept function. This could be worked around if bases and craft have a getExpectedPosition(ticks) which returns where we expect an object to be X ticks in the future.

 

Also, other events the scripts may need to respond to are:

1. Destruction of home base.

2. Destruction of UFO. (response: return to base, or resume patrol)

3. Loss of contact with UFO. (response: probably to follow projected path, looking for UFO.)

4. User issues new orders to craft

Link to comment
Share on other sites

I think we should focus on the Iteration 2 goals, not on things we don't need atm. Always think of the YAGNI rule - You Aren't Gonna Need It.

Also, other events the scripts may need to respond to are:

1. Destruction of home base.

2. Destruction of UFO. (response: return to base, or resume patrol)

3. Loss of contact with UFO. (response: probably to follow projected path, looking for UFO.)

4. User issues new orders to craft

That Craft simulation gets complicated.

 

The solution to those 4 situations (that I propsed on IRC) is to have circular references, whenever something starts to involve something else in something's actions. Ie. Base has a list of references to Crafts docked at it, and each craft has a reference to home base. Craft has a reference to target, while target has list of references to crafts that target it. That way anything happenes, the destroyed object can inform all interested parties about its destruction. Signals can be helpful here.

 

Maybe I simply can't think in concurrency, but the simplest solution that comes to my mind is without any tasklets and concurrency, with simple World update that is run every frame.

Link to comment
Share on other sites

After two hours of coding, I have the following test which runs fine:

 

class CraftTestActor(model.craftactor.CraftActor):
   def main(self):
       self.setTarget(model.Geoposition(1.5, 1.5))
       self.setVelocity(500)
       self.waitUntilTargetReached()

This is the actor and here is the code which launches this actor:

craft = CraftTestActor("Test Craft")
craft.setPosition(model.Geoposition(0, 0))
craft.start()
       
for i in xrange(50):
   snake.pyos.watchdog()
   craft.tick()

# Craft current distance to target, which is coordinates [1.5, 1.5]
d = craft.getPosition().getDistance(model.Geoposition(1.5, 1.5))
assert d < 10

 

And even better, I can pickle the CraftTestActor (which contains a Geoposition class writed with C++) in the middle and unpickle it and it works just like nothing ever happend!

 

- Garo

Link to comment
Share on other sites

Wanna see some more cool stuff?

 

class ConnectSenderTestActor(model.actor.Actor):
   def Action_Act(self, other):
       print "ConnectSenderTestActor Acted!"
       self.acted = True

class ConnectListenerTestActor(model.actor.Actor):
   def Action_OnAct(self, other):
       other = model.World.getActor(other)
       print "ConnectListenerTestActor: %s Acted!" % other.name
       self.acted = True


class World_TestCase(unittest.TestCase):
   def test_connectFromToWorks(self):
       
       sender = ConnectSenderTestActor("sender actor")
       listener = ConnectListenerTestActor("listener actor")
       
       model.World.connect(sender, "Act", listener, "OnAct")
       
       sender.Action_Act(None)
       
       self.assertEqual(sender.acted, True)
       self.assertEqual(listener.acted, True)
       

 

This is the Action feature. Actors can have Actions.

Each action can be connected to another action (or many actions). This can be done by the Actor itself, or be done without the Actor newer knowing it (connections could be loaded from XML files which are created by some kind of World Editor, for example)

 

Example: BaseActor has addCraft, which is used to bind a CraftActor to the base. the addCraft connects the craft destroyed -action to base own OnCraftDestroyed, which then removes the craft from the base craft list. This way the base can always know if one if its crafts are destroyed on action and then remove the craft from it.

 

What do you guys think?

 

- Garo

Link to comment
Share on other sites

Wanna see some more cool stuff?

 

class ConnectSenderTestActor(model.actor.Actor):
   def Action_Act(self, other):
       print "ConnectSenderTestActor Acted!"
       self.acted = True

class ConnectListenerTestActor(model.actor.Actor):
   def Action_OnAct(self, other):
       other = model.World.getActor(other)
       print "ConnectListenerTestActor: %s Acted!" % other.name
       self.acted = True


class World_TestCase(unittest.TestCase):
   def test_connectFromToWorks(self):
       
       sender = ConnectSenderTestActor("sender actor")
       listener = ConnectListenerTestActor("listener actor")
       
       model.World.connect(sender, "Act", listener, "OnAct")
       
       sender.Action_Act(None)
       
       self.assertEqual(sender.acted, True)
       self.assertEqual(listener.acted, True)
       

 

This is the Action feature. Actors can have Actions.

Each action can be connected to another action (or many actions). This can be done by the Actor itself, or be done without the Actor newer knowing it (connections could be loaded from XML files which are created by some kind of World Editor, for example)

 

Example: BaseActor has addCraft, which is used to bind a CraftActor to the base. the addCraft connects the craft destroyed -action to base own OnCraftDestroyed, which then removes the craft from the base craft list. This way the base can always know if one if its crafts are destroyed on action and then remove the craft from it.

 

What do you guys think?

 

- Garo

What can I say? Awesome. That's the power of a reflection in a language :)

Link to comment
Share on other sites

Reflection is cool, however be careful that the real use of reflection must be hidden inside a well designed and small space... Do not under any circunstance let reflection being everywhere arround in the code.

 

On the other hand, packaged accordingly the mechanism you are using is very cool. ;)

 

Greetings

Red Knight

Link to comment
Share on other sites

  • 2 weeks later...

For those of you who do not know me, I am Maverick, I was active in this project long ago. I have come upon some spare time these days and I was wondering a few things specifically about your interception method. It is probably imprudent of me to jump straight in to questioning, but i believe it will bear itself out in the end. To begin:

 

1) Do you keep velocity vector information about generated ships, and if so, how do you consider it? (by velocity vector information i refer to both speed and direction, "consider" refers to coordinate system: cartesian, spherical, etc)

 

2) How are you currently "path finding" for the intercepting ship?

 

I ask these two questions in order to point out that a) velocity vector information (of sorts) was included in the hyperwave transmission dialogue of the original xcom -- suggesting that they took it into account, (B) to point out that if you simply have your ship "chase" the UFO, even on a small time frame it wastes fuel which is especially noticable when you consider the speed difference between the alien ships and the basic terran ships, and © to point out that following from (a) and (B) the solution is to target the projected end point of the UFO. This is a further complication however, because it requires then that regardless of player selected game speed, each "move" of each ship must be calculated along the smallest possible tick length (which technically you should be doing anyhow -- if you allow the ships to only alter direction at each tick corresponding to user defined speed, the UFO will travel in an erratic pattern in trying to find it's way to a point.)

 

The simplest example I can give is that if a player has the time set to 1 hour (game time) per 1 second (real time) then a ship should make 720 course corrections per game "tick".

 

-Maverick

Link to comment
Share on other sites

1) Do you keep velocity vector information about generated ships, and if so, how do you consider it? (by velocity vector information i refer to both speed and direction, "consider" refers to coordinate system: cartesian, spherical, etc)

 

2) How are you currently "path finding" for the intercepting ship?

 

Those both are interesting questions and we haven't done anything for them... yet.

In the current iteration, the ships will simply set to target the current position of the craft which is to be followed. This needs that the interceptor has greater speed than the ufo which it will chase. This is enough for now, because we just want to see a ship chasing an UFO. :)

 

In the future iterations we will take into account ships ominal properties, like max speed, agility, fuel consuption and we'll also add more complex and better "autopilots" for the ships, which will allow the crafts to do complex manoveurs etc.

 

And oh, it's nice to see that old members are still following our progress. If you have some spare time, why not joun us into the IRC channel #xenocide at freenode? =)

 

- Garo

Link to comment
Share on other sites

Hey RK, I've been well. I'm still in school, have another year to go, but I have some time for sure at least until October -- so I'll see what I can do in that time. I will happily volunteer any math skills I may have for whatever needs them.

 

-Maverick

Link to comment
Share on other sites

Currently implemented storylines:

* UFO is randomly spawned over the Globe
* Player has one Base randomly placed on the Globe
* UFO move straight to the randomly choosen destination, then is destroyed
* Only one UFO on the globe
* Player can rotate and zoom the Globe
* The Sun rotates around the Globe
* Game time handling

 

- Garo =)

Link to comment
Share on other sites

Awesome! Sounds like it's time for a release? :rolleyes:

Yep. Besides I'd still like to heard a word from guyver6, the code needs a bit refactoring but in my opinion, we can release at monday or tuesday =)

 

- Garo

Link to comment
Share on other sites

I'm not really able to compile Iteration 2 myself right now. Could anyone send me a link to a compiled version min. 24 hours prior to scheduled publication? That way I will be able to write some nice stuff... :)
Link to comment
Share on other sites

I'm not really able to compile Iteration 2 myself right now. Could anyone send me a link to a compiled version min. 24 hours prior to scheduled publication? That way I will be able to write some nice stuff... :)

edit: removed link for now.

 

- Garo

Edited by Garo
Link to comment
Share on other sites

×
×
  • Create New...