Chapter 12. Multithreaded Game Programming

When games are running on a mobile device, many different pieces of code must be executed simultaneously. This includes actions such as checking the keyboard, moving the sprites, detecting sprite collision, planning the game's artificial intelligence, and of course, repainting. If a developer has to put all those tasks into a list and execute the list step-by-step, just waiting for a new key-pressed event could pause the game. A much better approach would be to put tasks into different lists, and permit each list to execute at the same time. For example, one task can check the keyboard as the other task executes other parts of the code. Fortunately, when calling the repaint() method, only a request for a new painting is sent, without waiting for the repaint itself. This is done in a completely different task. A question arises: Can you do such a thing in J2ME? The answer is yes. This multitasking capability can be achieved simply by using Java's multithreading capabilities.

Threads

A thread is a path taken by a program during execution. By executing through several paths, an application is quicker, flexible, and non-blockable. If a program can be split into separate tasks, it is often easier to program the algorithm as a separate task or thread. Such programs deal with multiple independent tasks. The popularity of threading increased when graphical interfaces became the standard for all devices, from desktop computers to small intelligent devices. The reason for threading's success is the user's perception of better program performance—and performance is extremely important in games!

When developing threads for games, the following questions arise:

  • How many threads does the game need?

  • What should be the animation frame-rate?

  • How do you manipulate data using a thread?

  • Do you use the Thread class, Runnable interface, or Timer class?

The answer to the first question is that at least three threads are required. One thread is used for drawing the graphic scene, one for reading input events (for example, keyboard, pen, and so on), and one for manipulating the sprites. Fortunately for the game developers, the first two threads are already automatically implemented, and developers don't need to implement additional threads. The easiest way to implement the third thread is by using the Canvas class that represents the game graphics area where the game is happening. Additional threads can be used for artificial intelligence to make the game more attractive.

If a developer wants to attract a gaming audience, a lot of work must be done in the area of the graphics. Usually today's games make a lot of effort to convince players by making magnificent introductory animations. But Java-enabled mobile devices are too limited in resources to offer the same effects. Mobile developers need to focus more on the games themselves. Providing animated action is possible with implementation of the animation thread. Such a thread is used to offer animated scenes with a fixed frame-rate.

Animation adds a great deal to the user interface, but unfortunately the devices currently available on the market are not fast enough, so higher animation frame-rates are not possible. There are many ways to implement animation, which can differ in speed from device to device. This book will show you the simplest solution: taking the lowest common denominator. The lowest acceptable frame-rate is ten frames per second. This means that a thread must execute the loop every 100 milliseconds.

The thread can have multiple methods to manipulate its data, but it should have these two required methods:

  • start()—. To start the thread (game)

  • stop()—. To stop the thread (game)

Extending the Thread Object

Within the Java virtual machine, a java.lang.Thread object encapsulates the details of how a particular system approaches the multithreading.

The following methods are important for the Thread class:

  • currentThread()—. This method returns a reference to the currently executing thread object.

  • yield()—. This method causes the currently executing thread object to temporarily pause and permit other threads to execute.

  • sleep(long millis)—. This method causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds. The thread does not lose ownership of any monitors. This method is very useful for managing the game speed that must produce a fixed frame-rate. The developer's responsibility is to measure the time of execution and to sleep the rest of the time to achieve the correct time (for example, 100 milliseconds for 10 frames per second).

  • start()—. This method causes the thread to begin execution. The Java Virtual Machine calls the run method of this thread. The result is that two threads are running concurrently: the current thread (which returns from the call to the start method) and the other thread (which executes its run method). The start method is very important for starting the game.

  • run()—. Subclasses of Thread should override the run() method to provide functionality such as sprite movement, calling the repaint() method and executing artificial intelligence tasks.

  • isAlive()—. This method tests whether the thread is alive. A thread is alive if it has been started and has not yet died.

  • setPriority(int newPriority)—. This method changes the priority of the thread.

  • getPriority()—. This method returns the thread's priority.

  • activeCount()—. This method returns the current number of active threads in the Java Virtual Machine.

  • join()—. This method waits for the thread to die.

Listing 12.1 illustrates how to implement a thread using the Thread class.

Example 12.1. The GameCanvas Thread Example

public class GameCanvas
{
  private final int DELAY = 100;

  private GameThread thread;
  private boolean running;

  public GameCanvas()
  {
  }

  public void start()
  {
    running = true;
    thread = new GameThread();
    thread.start();
  }

  public void stop()
  {
    running = false;
  }

  public class GameThread extends Thread
  {
    public void run()
    {
      while (running)
      {
        // Move sprites
        // Check collisions
        // Repaint
        try
        {
          Thread.sleep(DELAY);
        }  catch (Exception ex) { }
      }
    }
  }
}

A special flag called running is used to notify the thread whether it can continue. When the flag is set to false by a stop() method, the thread finishes. The constant DELAY defines the sleep time for each loop. The developer should be aware that sleep time alone doesn't provide the animation frame-rate because execution time also contains times needed for moving sprites, collision detection, and calling the paint() method, which is executed in a separate thread. In reality, the speed is much lower, depending on the CPU speed of the mobile device.

Implementing the Runnable Interface

Another way to create a thread is to implement the java.lang.Runnable interface. This interface is designed to provide a common protocol for objects that want to execute code while they are active. For example, Runnable is implemented by the Thread class. Being active simply means that a thread has been started and has not yet been stopped. In addition, Runnable provides the means for a class to be active while not subclassing Thread. The class is free to extend some other class, in this case Canvas. A class that implements Runnable can run without subclassing Thread by instantiating a Thread instance and passing itself in as the target. In most cases, the Runnable interface should be used if you are only planning to override the run() method and no other Thread methods.

Runnable has only one method:

  • run()—. When an object implementing the Runnable interface is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread. The general contract of the run method is that it may take any action whatsoever. Usually, a game provides its own functionality within this method (such as sprite movement and calculations).

Listing 12.2 shows how to implement a thread using the Runnable interface.

Example 12.2. The GameCanvas Runnable Example

public class GameCanvas implements Runnable
{
  private final int DELAY = 100;

  private Thread thread;
  private boolean running;

  public GameCanvas()
  {
  }

  public void start()
  {
    running = true;
    thread = new Thread(this);
    thread.start();
  }

  public void stop()
  {
    running = false;
  }

  public void run()
  {
    while (running)
    {
      // Move sprites
      // Check collisions
      // Repaint
      try
      {
        Thread.sleep(DELAY);
      }  catch (Exception ex) { }
    }
  }
}

As with the extended Thread object, a special flag called running is also used to notify the thread whether it can continue. When the flag is set to false by a stop() method, the thread finishes. The developer should be aware that, as in the Thread class, sleep time alone doesn't provide the animation frame-rate. If developer wants to have a fixed frame-rate, thus freezing the execution of every frame at one tenth of a second, the easiest approach is to use the Timer class, which will be discussed later in this chapter. Timers may not be connected to the hardware clock itself, which can fire the timer events on time.

Thread Priorities

Each thread can have its own thread priority. By looking closer at the multithreading mechanism implemented in the Java virtual machine, you see that only one thread is ever executed at a given time (on single-processor machines). It is the system's responsibility to slice the time and execute the specific thread within one time slice. How often the thread appears in a specific time is indicated by the priority. A higher priority means more frequent execution, and such threads also run faster. The following constants define priorities:

  • MIN_PRIORITY—. The minimum priority that a thread can have (value 1)

  • NORM_PRIORITY—. The default priority assigned to a thread (value 5)

  • MAX_PRIORITY—. The maximum priority that a thread can have (value 10)

The priority can be any integer number between MIN_PRIORITY and MAX_PRIORITY. If it is out of this range, java.lang.IllegalArgumentException is thrown.

Thread States

Depending on what the thread is doing, it can have different thread states. Each thread can have one of four states:

  • New state—This is the state of a newly created thread. The start() method is used to activate the thread, assign some resources to it, and move it to a runnable state. A newly started thread always executes a run() method.

  • Runnable state—In this state, the thread is on the virtual machine's list of runnable threads. When it gets to run depends on its priority, the characteristics of the JVM, and the activity of other threads.

  • Blocked state—A thread can be moved to the list of blocked threads as a result of entering a wait() method, calling the sleep() method, or calling one of the blocking I/O methods that the JVM manages.

  • Dead state—A thread becomes dead when it exits the run() method that it started. A dead thread can't be reanimated.

By using the isAlive() method, the developer can find out if the thread is still alive. A thread is alive if it is starting, running, or blocked. Dead threads are not alive, and the call to the method returns false in this case.

Synchronizations and Deadlocks

In theory, any Thread can access any object within a Java program. To prevent the programming chaos that would result from multiple threads modifying the same object at the same time, Java uses the monitor mechanism wherein the synchronized keyword is used. The monitor mechanism is built into the Object class, thus insuring that all Java objects can use it.

The Java virtual machine has control over a lock variable attached to each object. This lock variable is used to implement a monitor mechanism that can control threads' access to the object. The monitor mechanism is used only when the synchronized keyword has been used to label a block of code. When a thread attempts to enter a synchronized block of code, the virtual machine checks whether the lock is available. If no other thread has the lock, the current thread locks the object. If the lock already exists, the current thread becomes blocked and can't proceed until the lock is released. When the thread leaves the synchronized block, the lock is automatically released, and the next blocked thread in a blocked list may proceed.

A deadlock occurs when two threads are trying to gain control of object and each one has a lock on a resource that the other needs to proceed. Unfortunately, Java has no mechanism to detect or control deadlock situations. It is a programmer's responsibility to plan objects and threads so that after the thread acquires the lock on the object, it will be able to complete the synchronized code, or at least call its wait() method.

wait() and notify()

If two threads require more cooperation in the use of an object that can't be obtained with a simple synchronized access, the wait() and notify() methods can be used.

The wait() method has three forms:

  • wait()—. This method causes the current thread to wait until another thread invokes either the notify() method or the notifyAll() method for this object. The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up, through a call to either the notify() method or the notifyAll() method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution. Only a thread that is the owner of this object's monitor should call this method.

  • wait(long timeout)—. This method causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.

  • wait(long timeout, int nanos)—. This method causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object, some other thread interrupts the current thread, or a certain amount of real time has elapsed.

The notify() method wakes up a single thread that is waiting on the object's monitor. If any threads are waiting on the object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. The thread waits on an object's monitor by calling one of the wait methods. The awakened thread will not be able to proceed until the current thread relinquishes the lock on the object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on the object. The notifyAll() method wakes up all threads that are waiting on the object's monitor.

Timers

If you want to execute tasks that take different amounts of time during sprite movement and collision detection, the simplest way is to use the java.util.Timer class. The Timer class makes it easy for threads to schedule tasks for future execution in a background thread. Tasks may be scheduled for one-time execution, or for repeated execution at regular intervals. Timer tasks should complete quickly. If a timer task takes excessive time to complete, it “hogs” the timer's task execution thread. This can, in turn, delay the execution of subsequent tasks, which might bunch up and execute in rapid succession when the offending task finally completes. By default, the task execution thread does not run as a daemon thread, so it is capable of keeping an application from terminating. If a caller wants to terminate a timer's task execution thread rapidly, the caller should invoke the timer's cancel() method.

The following methods are part of the Timer class:

  • schedule(TimerTask task, long delay)—. This method schedules the specified task for execution after the specified delay. Games should call this method when execution of the task doesn't depend on fixed execution rate. Each task is executed within a specified amount of time (for example, every 100 milliseconds), but the execution of the task can exceed that time.

  • schedule(TimerTask task, Date time)—. This method schedules the specified task for execution at the specified time.

  • schedule(TimerTask task, long delay, long period)—. This method schedules the specified task for repeated fixed-delay execution, beginning after the specified delay. Subsequent executions take place at approximately regular intervals separated by the specified period.

    If the execution is delayed for any reason (such as garbage collection or other background activity), subsequent executions will be delayed as well. In the long run, the frequency of execution will generally be slightly lower than the reciprocal of the specified period.

  • schedule(TimerTask task, Date firstTime, long period)—. This method schedules the specified task for repeated fixed-delay execution, beginning at the specified time. This is similar to preceding method.

  • scheduleAtFixedRate(TimerTask task, long delay, long period)—. This method schedules the specified task for repeated fixed-rate execution, beginning after the specified delay. Subsequent executions take place at approximately regular intervals, separated by the specified period.

    If the execution is delayed for any reason, two or more executions will occur in rapid succession to catch up. In the long run, the frequency of execution will be the exact reciprocal of the specified period.

    A game should use this method if the timeframe must be fixed. However, if the execution time of the task is larger than is allowed, the number of threads will constantly increase. J2ME devices are unfortunately resource-limited, and might eventually crash.

  • scheduleAtFixedRate(TimerTask task, Date firstTime, long period)—. This method schedules the specified task for repeated fixed-rate execution, beginning at the specified time. This is similar to the preceding method.

  • cancel()—. This method terminates this timer, discarding any currently scheduled tasks. It does not interfere with a currently executing task if it exists. After a timer has been terminated, its execution thread terminates gracefully, and no more tasks can be scheduled on it.

You need to implement your own TimerTask class and create a TimerTask object if you want to use the Timer class. The run() method needs to be implemented containing the thread functionality. Listing 12.3 contains an implementation of threading using the Timer class.

Example 12.3. The GameCanvas Timer Example

import java.util.*;

public class GameCanvas
{
  private final int DELAY = 100;

  private Timer timer;

  public GameCanvas()
  {
  }

  public void start()
  {
    GameTask task = new GameTask();
    timer = new Timer();
    timer.scheduleAtFixedRate(task, 0, DELAY);
  }

  public void stop()
  {
    running = false;
  }

  public class GameTask extends TimerTask
  {
    public void run()
    {
      // Move sprites
      // Check collisions
      // Repaint
    }
  }
}

This code is similar to the implementation of the Thread class, where the developer writes Thread's own inner class. Timer executes the task every 100 milliseconds. If the task takes more then 100 milliseconds, a new task will still be invoked on time. This means that at a specific time, there might be a bunch of threads running, which can slow down the device. Another approach would be to use the schedule() method, but the application would lose the fixed scheduling necessary during game execution.

Making Threads Better

The problem with timers is that they don't provide additional control methods for tasks like threads do. You can still use the Thread class instead by implementing a similar functionality as found in the Timer class, as seen in Listing 12.4.

Example 12.4. A Similar Functionality to the Timer Example

public class GameCanvas
{
  private final int DELAY = 100;

  private GameThread thread;
  private boolean running;

  public GameCanvas()
  {
  }

  public void start()
  {
    running = true;
    thread = new GameThread();
    thread.start();
  }

  public void stop()
  {
    running = false;
  }

  public class GameThread extends Thread
  {
    public void run()
    {
      while (running)
      {
        long time = System.currentTimeMillis();
        // Move sprites
        // Check collisions
        // Repaint
        time = System.currentTimeMillis() - timer;
        try
        {
          if (time < DELAY)
              Thread.sleep(DELAY - (int)time);
        }
        catch (Exception ex) { }
      }
    }
  }
}

In the run() method, you measure the time of functionality execution by calling the System.currentTimeMillis() method. If the execution time is less then 100 milliseconds, the run method sleeps until it receives a reminder. Otherwise, it executes the next loop.

Summary

Through the use of multithreading, games become faster and manage different tasks at the same time—which was not easy to achieve in older systems. While the game waits on the player's keyboard, joystick, or touch-screen commands, it can paint on the screen, calculate new positions of the sprites, and manage its artifical intelligence. Multithreading can be done using threads or timers. Timers are much easier to use, because a game can execute the task in a fixed timeframe. However, because Java did not initially have the Timer class, a lot of ported games opt to use the Thread class instead. The next chapter will introduce high-level GUI components, and how to use them for game development.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.217.5