Monday, August 23, 2010

Combat and concurrency

It was on July 15th when I titled a blog post "Combat in sight?", but now it really is. Today, I committed a version to my project where players can attack and thus deal damage. Blocking is not possible yet, but that's more a matter of work and not of thinking. Players still can't loose the game, but that's another story...

Now, for those of you not familiar with "concurrency" in programming, it means when multiple parts of a program run at the same time, "competing for the computer's resources" like CPU or RAM. Concurrency is hard for three reasons:
  • you have to control what happens when, because the classical "everything happens in a fixed sequence" is not true
  • speaking of "competing for resources", when two concurrent threads of execution access the same data, they can overwrite each other, messing things up
  • for this reason, locks and synchronizing have come up, which grant exclusive access to a resource to a single thread. This also means that threads have to wait for resources until they aren't locked any more. In the worst case, two threads can wait for each other.
Laterna Magica uses an approach that has two main threads; one for the game, and one for the user interface. While the UI thread is mandated by Java, I could have implemented LM to run on the same thread. The main reason I did not was to make LM independent of the GUI, meaning that the program can use different UIs and even run without a User Interface, say in a Computer-vs-Computer simulation or on a server (while the latter needs more preparation, I believe it will be possible someday).

So there are two threads, which means every GUI interaction means synchronization. The first GUI interaction implemented was playing cards and activating abilities. The code to enable that was pretty wordy and hardly extensible:


private boolean    ready;
private PlayAction a;

public GuiActor(Player player) {
    super(player);
} 
 
public synchronized void putAction(PlayAction a) {
    ready = true;
    this.a = a;
    notifyAll();
}

public synchronized PlayAction getAction() {
    ready = false;
    while(!ready)
        try {
            wait();
        } catch(InterruptedException ex) {
            ex.printStackTrace();
        }
    return a;
}

On the surface, it doesn't look that bad, but it has several issues:
  • It is low-level: It handles all the synchronization stuff itself, as seen by boolean ready, notifyAll(), synchronized, while(!ready) ... This is all stuff that has to be repeated for every sort of user input: declare attacker, blocker, ...
  • It moves control away from the actor: putAction takes a PlayAction, which is the result of e.g. clicking on a card. How that click is associated to the PlayAction is not controlled by the actor which violates atomicity
    • Translating click to PlayAction is specific to playing spells and not compatible with e.g. declaring attackers. This means that the translation, which is not atomic, has to be performed for every type of input
  • Missing Encapsulation: The interpretation as a PlayAction is only valid when the player has priority, and this fact is not represented here: There is no representation of the fact whether the Actor is waiting for a PlayAction or for declaring an attacker
On the opposite, here is the new variant:

public class GuiMagicActor extends AbstractMagicActor {
    public final GuiChannels    channels = new GuiChannels();
   

    private static T getValue(Fiber f, Channel ch, GuiActor a) {
        a.start();
        log.debug("Waiting for result...");
        T result = Parallel.getValue(f, ch);
        log.debug("received!");
        a.dispose();
        return result;
    }
   
    public PlayAction getAction() {
        return getValue(channels.fiber, channels.actions, new ActionActor(this));
    }

    
    ...
}

This new version is based on the Jetlang concurrency framework, which works using channels and callbacks: Messages published to a channel are - asynchronously - forwarded to registered callbacks, which may then in turn publish new messages. The Parallel.getValue() method is used to get a value back from the channel synchonously, and the ActionActor encapsulates the state, in this case choosing a PlayAction to perform:

public class GuiChannels {
    /**
     * Channel for receiving {@link PlayAction}s to execute when the player has priority
     */
    public final Channel  actions      = new MemoryChannel();
   
    /**
     * Channel for publishing {@link MagicObject}s when the user clicks on them
     */
    public final Channel objects      = new MemoryChannel();
   
    /**
     * Channel for publishing {@link Player}s when the user clicks on them
     */
    public final Channel      players      = new MemoryChannel();
   
    /**
     * Channel for publishing when the user clicks "pass priority"
     */
    public final Channel        passPriority = new MemoryChannel();
   
    public final Fiber                fiber;
   
    public GuiChannels() {
        PoolFiberFactory f = new PoolFiberFactory(Executors.newCachedThreadPool());
        fiber = start(f.create());
    }
   
    private Fiber start(Fiber f) {
        f.start();
        return f;
    }
}


public class ActionActor extends GuiActor {
    public ActionActor(GuiMagicActor actor) {
        super(actor);
    }
   
    @Override
    public void start() {
        disposables.add(actor.channels.objects.subscribe(actor.channels.fiber, new CardCallback()));
        disposables.add(actor.channels.passPriority.subscribe(actor.channels.fiber, new PassPriorityCallback()));
    }
   
    private class CardCallback implements Callback {
        @Override
        public void onMessage(MagicObject c) {
            log.debug("Received: " + c);
            PlayAction a = GuiUtil.getActionOptional(actor.getPlayer(), c);
            if(a != null) actor.channels.actions.publish(a);
        }
    }
   
    private class PassPriorityCallback implements Callback {
        @Override
        public void onMessage(Void v) {
            log.debug("Received pass priority");
            actor.channels.actions.publish(null);
        }
    }
}
 

The ActionActor receives Messages through the objects and passPriority channels and reacts specific to its task.

Hope you liked it!

    No comments: