Question

This example has been used in another question to illustrate how coroutines can be used to script cutscenes in a video game:

bob.walkto(jane)
bob.lookat(jane)
bob.say("How are you?")
wait(2)
jane.say("Fine")
...

Each function yields to the main engine which does animation, timing, etc. before resuming the coroutine. A possible alternative to coroutines would be an event queue instead of code, but then one has to implement control logic and loops as events. Are there any other alternatives to coroutines that can be used to implement this kind of functionality? I've seen callbacks mentioned in some articles, but I'm not sure how the code would look.

Was it helpful?

Solution

Coroutines are well suited for this since you get to keep all your local state variables with no hassle. I.e. without having to manually store it in a context somewhere.

But I don't see an event system as an alternative. More as a supplement that you will most likely want to have in addition to a coroutine-based scripting system.

Example (in somewhat coherent C++):

You have implemented a behavior using coroutines along these lines:

class EnterHouse : public NPCBehavior
{
    EnterHouse(House theHouse) { _theHouse = theHouse; }
    void Begin() { _theHouse.AddNPC(NPC()); }
    void Update()
    {
        NPC().WalkTo(_theHouse.GetDoor().Position());
        NPC().FaceObject(_theHouse.GetDoor());
        NPC().PlayAnimation(NPC().Animations().Get(eAnimKnockOnDoor));
        Sleep(1.0f);       
        NPC().OpenDoor(_theHouse.GetDoor());
    }
    void End() { /* Nothing */ }

    private House _theHouse;
}

Imagine that the methods on the NPCs will themselves create NPCBehavior objects, push them on some sort of behavior stack and return from the call when those behaviors complete.

The Sleep(1.0f) call will yield to your script scheduler and allow other scripts to run. The WalkTo, FaceObject, PlayAnimation and OpenDoor will also call Sleep to yield. Either based on a known animation duration, to wake up periodically to see if the pathfinder and locomotion system are done walking or whatever.

What happens if the NPC encounters a situation he will have to deal with on the way to the door? You don't want to have to check for all these events everywhere in your coroutine-based code. Having an event system supplement the coroutines will make this easy:

An trashcan topples over: The trashcan can broadcast an event to all nearby NPCs. The NPC object decides to push a new behavior object on his stack to go and fix it. The WalkTo behavior is in a yielding Sleep call somewhere, but now the FixTrashcan behavior is running due to the event. When FixTrashcan completes the WalkTo behavior will wake up from Sleep and never know about the trashcan incident. But it will still be on its way to the door, and underneath it we are still running EnterHouse.

An explosion happens: The explosion broadcasts an event just like the trashcan, but this time the NPC object decides to reset it running behaviors and push a FleeInPanic behavior. He will not return to EnterHouse.

I hope you see what I mean by having events and coroutines live together in an AI system. You can use coroutines to keep local state while still yielding to your script scheduler, and you can use events to handle interruptions and keep the logic to deal with them centralized without polluting your behaviors.

If you haven't already seen this article by Thomas Tong on how to implement single-threaded coroutines in C/C++ I can highly recommend it.

He uses only the tiniest bit of inline assembly (a single instruction) to save the stack pointer, and the code is easily portable to a whole bunch of platforms. I've had it running on Wintel, Xbox 360, PS3 and Wii.

Another nice thing about the scheduler/script setup is that it becomes trivial to starve off-screen or far-away AI characters/scripted objects if you need the resources for something else. Just couple it with a priority system in you scheduler and you are good to go.

OTHER TIPS

You didn't mention what language you were using, so I'm going to be writing this in Lua with object orientation provided by middleclass - https://github.com/kikito/middleclass (disclaimer: I'm middleclass' creator)

Another option would be splitting your cutscenes into "lists of actions". This will probably blend better with your code, if you already have a game loop that invokes an 'update' method on lists of objects.

Like this:

helloJane = CutScene:new(
  WalkAction:new(bob, jane),
  LookAction:new(bob, jane),
  SayAction:new(bob, "How are you?"),
  WaitAction:new(2),
  SayAction:new(jane, "Fine")
)

Actions would have a status attribute with three possible values: 'new', 'running', 'finished'. All the "action classes" would be subclasses of Action, which would define start and stop methods, as well as initialize the status to 'new' by default. There would be also a default update method that throws an error

Action = class('Action')

function Action:initialize() self.status = 'new' end

function Action:stop() self.status = 'finished' end

function Action:start() self.status = 'running' end

function Action:update(dt)
  error('You must re-define update on the subclasses of Action')
end

Subclasses of Action can improve upon those methods, and implement update. For example, here's WaitAction:

WaitAction = class('WaitAction', Action) -- subclass of Action

function WaitAction:start()
  Action.start(self) -- invoke the superclass implementation of start
  self.startTime = os.getTime() -- or whatever you use to get the time
end

function WaitAction:update(dt)
  if os.getTime() - self.startTime >= 2 then
    self:stop() -- use the superclass implementation of stop
  end
end

The only missing implementation part is CutScene. A CutScene will mainly have three things: * A list of actions to execute * A reference to the current action, or the index of that action on the action list * An update method like the following:

function CutScene:update(dt)
  local currentAction = self:getCurrentAction()
  if currentAction then
    currentAction:update(dt)
    if currentAction.status == 'finished' then
      self:moveToNextAction()
      -- more refinements can be added here, for example detecting the end of actions
    end
  end
end

With this structure, the only thing you need is your game loop calling helloJane:update(dt) on every loop iteration. And you eliminate the need of coroutines.

Callbacks (C#-style pseudocode):

bob.walkto(jane, () => {
    bob.lookat(jane), () => {
        bob.say.....
    })
})

Definitely not the most convenient way.

A different approach is Futures (also known as promises):

futureChain = bob.walkto(jane)
  .whenDone(bob.lookAt(jane))
  .whenDone(...)
  .after(2 seconds, jane.Say("fine"));

futureChain.run();

One interesting language to look at is E - it has built-in support for futures, with a nicer syntax than above.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top