Scripting Game Objects

Part 2

In this guide we look at the game object class that we want to be scripted. In the example we have been looking at this is the Helocopter class.

Not all the source code is shown here I have cherry-picked those parts I feel need explanation, standard Processing methods I leave with you to look up in the reference.

We will start with some of the important class attributes.

  // Store for events waiting to be processed
  LinkedList event_queue = new LinkedList();

  // The variable name to be used in the script
  final String identifier;
  Script script;

The identifier attribute is the variable name to be used inside the script, in this example it was helo. The actual QScript algorithm will be stored in the script attribute.

The event_queue needs a little explanation. The script is going to be evaluated in a separate thread where every now and again the script will fire events to be pickupup in the main thread. These events are asynchronous so could occur at any time in the main thread. To prevent concurrent modification exceptions this linked list is used to simply store the fired events in FIFO order until they can be processed safely in the main thread.

NOTE: If you are using Java Swing this is not necessary - look at the Java Sing QScript editor source code to see what I mean.

Let's look at the constructor -

  /**
   * Create the helicopter.
   * @param name the variable identifier used in the script
   * @param bodyFile image file for body
   * @param rotorFile image file for rotor
   */
  Helicopter(String name, String bodyFile, String rotorFile) {
    identifier = name;
    rotor = loadImage(rotorFile);
    body = loadImage(bodyFile);
    pos = new PVector(width/2, height/2);
    targetPos = pos.get();
    velocity = new PVector();
    script = new Script("");
    script.storeVariable(new Thing(identifier, this));
    script.addListener(this);
    registerMethod("pre", this);
  }
  

In line 44 the variable identifier is set and in line 50 we create an empty script for this object. Line 51 is important because it puts the 'variable' in the data store for this script. If we didn't do this we would get an uninitialised variable error during evaluation.

Line 53 is a Processing method used in many libraries but less commonly used inside a sketch. Once this statement has been executed then Processing will look for and execute the pre method just before it does the draw method. This class uses the pre method for processing events stored in event_queue.

  public void pre() {
    int count = 0;
    while (!event_queue.isEmpty () && count++ < 20)
      performEventAction(event_queue.removeFirst());
  }

This method will process a maximum of 20 events from the FIFO queue. Since a script might generate numerous events this method limits how many will be processed in one go so tha main thread will not become unresponsive. You might want to try different cap values in your own sketches.

This method is usd to capture the events fired from the solver thread

  @EventHandler
  synchronized public void onScriptEvent(ScriptEvent event) {
    if (event instanceof HaltExecutionEvent && event.etype == ErrorType.STOPPED_BY_USER)
      event_queue.addFirst(event);
    else
      event_queue.addLast(event);
  }

This method is usd to capture the events fired from the solver thread. The method must be annotated with @EventHandler and be synchronized. This is a FIFO queue so events are added to the end of the queue, the only exception is the halt script event which needs to be processed immediately.

So events are captured by the onScriptEvent and placed on a queue to be removed later in the pre method when each event is then passed to the performEventAction method to take the appropriate action. Only the first part of this method is shown here but it comprieses of a number of if statements one for each event we want to process (others will be ignored). It is best that the most commonly fired events are tested for first. For instance in line 130 we test for the turn event and if found it looks at the extra information stored in the event. The first element is the helicopter we want to control but because each helicopter has its own script then this information is not used. (If we had a single script that controlled both helicopters then we would use this to decide on which to turm) In line 134 we retrieve the angle we want to rotate the helicopter by and this is passed to the turn method. Lines 136-141 do something similar for move events.

  public void performEventAction(ScriptEvent event) {
    if (event instanceof WaitEvent) {
      activity = "Waiting for " + " " + event.extra[0];
    } else if (event instanceof TurnEvent) {
      // extra[0] = is the helicopter : extra[1] is the angle in degrees
      // Helicopter thisOne = (Helicopter) event.extra[0];
      activity = event.getMessage();
      float angle = (Float) event.extra[1];
      turn(angle);
    } else if (event instanceof MoveEvent) {
      // extra[0] = is the helicopter : extra[1] is the distance in pixels
      // Helicopter thisOne = (Helicopter) event.extra[0];
      float dist = (Float) event.extra[1];
      move(dist);
      activity = event.getMessage();
    } ...

Finally we want to update the helicopters state (position and heading) every frame so the update method is called from the draw method. It has a single parameter which is the elapsed time (in seconds) since the method was last called. This provides animation independant of the frame rate so the sketch will appear to run at the same speed not matter how fast the hardware it is running on.

  void update(float elapsedTime) {
    updateAngle(elapsedTime);
    updatePos(elapsedTime);
  }

  /**
   * Update the rotation of the helicopter
   * @param elapsedTime the elapsed time in seconds
   */
  void updateAngle(float elapsedTime) {
    if (rotDir != 0) {
      float nextAng = angle + rotDir * ANGULAR_SPEED * elapsedTime;
      if ( (targetAngle - angle) * ( targetAngle - nextAng)  <= 0) {
        // Turn complete
        nextAng = targetAngle;
        rotDir = 0;
        script.resume();
      }
      angle = nextAng;
    }
  }

The update method calls the updateAngle method and the updatePos method (not shown here) to calculate the helicopters current state .

The source code for this sketch is well documented so as well as reading this guide I strongly recommend you examine it closely.

If you are interested in using scripted game objects in your sketchs tean get used to it, try adding a new method / event to change the speed of the helicopter. Good luck.