Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I start every instance of a certain class on a brand new thread?

I'm writing a game for fun where players have sprites that can jump around and shoot lasers. It can have up to three players. My class Sprite is the same for all three players, just has a different control layout for each depending on the player # it is given in its construction. Sprite uses a KeyListener to function.

In order for me to have multiple players doing things simultaneously (like shooting lasers or jumping) I need to have each Sprite object that is created in a separate thread. I know I can use implements Runnable on the Sprite class, however this only runs the code in the run() method on the new thread. This doesn't work because Sprite has keyPressed() and other such things in it that won't be on the new thread.

The one thought I had was to use a "helper" class and have that implements Runnable then in its run() method create the new Sprite object. This seems like a kind of messy approach however. Is there any way for me to create all new Sprite objects on a brand new thread (KeyListeners and all included on this thread)?

Code:

public class Sprite() implements KeyListener { //I want this class on a brand new thread
    int x;
    int y;
    int width;
    int height;
    Image spriteImage; 

    //code/methods for stuff

    //key listeners:

    @Override
    public void keyPressed(KeyEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void keyReleased(KeyEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void keyTyped(KeyEvent arg0) {
        // TODO Auto-generated method stub

    }

}

Current Solution:

public class SpriteStarter(/* Sprite class parameters go here */) implements Runnable{

    void run() {
        Sprite s = new Sprite(/*params*/);
    }

}

...

class Linker() {
    public static void main(String args[]) {
        SpriteStarter s1 = new SpriteStarter();
        SpriteStarter s2 = new SpriteStarter();
        Thread t1 = new Thread(s1);
        Thread t2 = new THread(s2);
        t1.start();
        t2.start();
    }
}

Edit:

Okay, after A LOT of great feedback, its become obvious to me that my game should be a single-threaded thing. I apologize for not realizing that, I haven't done much game programming so this was a new thing for me. My new thought is to have an ArrayList that on keyPressed() fires adds the pressed keys to the list. Than in the Sprite classes I'll have an update() method which looks at the pressed keys and updates coordinates accordingly. Update() will then be called at a fixed interval through a java.awt.Timer. This seems like it may work to me but I'm not sure so let me know! Thanks again to everyone. Also, I'd still appreciate an answer to the original question (being: how to start every instance of a class on a new thread) since it could be helpful for future programs.

like image 236
Ashwin Gupta Avatar asked Mar 10 '16 06:03

Ashwin Gupta


1 Answers

First off let's get something straight: objects don't run on threads. They don't run on anything, really. They sit in memory and wait for some thread to execute their methods. This is why you can have race conditions. Two threads may attempt to access the same memory (perhaps on the same object) at once. Onto your question.


Take a few breaths and think about the design. Your input is not multi-threaded (at least I'm guessing so). Events come in one by one to your application from some device on the operating system (or, based on your comment, from a framework abstraction such as a window panel). Usually, updating a sprite only involves trivial math. This can be done in-line on the thread feeding you events.

Further, you'll probably incur more overhead starting a thread for every new event (if you want to do what you're describing) than if you simply perform the calculation in-line. On top of that, what happens if you're in the middle of processing one event and a new one comes in? You'd need to block submission of the events to each thread (making the threads useless) or queue up events in thread-local queues.

But.. for entertainment.. let's say updating each sprite has the potential to take a very long time (this is silly, your game would be unplayable)...

You want one thread for each sprite. On each thread you need a message queue. When you start each thread you block on the message queue until a message arrives. When a message arrives the thread pops it off the queue and processes it. You need to encode the event in the message. The message needs to be passed by value into the queue. For simplicity, the message and the event can be the same class.

It would be easiest to just have one listener for events and have that listener dispatch the appropriate events to concerned sprites. But if you want each sprite to listen for its own events, you simply add them to the queue for the thread processing the sprite's events from within the sprite itself.

package sexy.multithreaded.sprites;

public class GameDriver implements EventListener {
    final EventDispatcher dispatcher;
    final Framework framework;
    final List<Sprite> sprites;

    GameDriver(Framework framework) {
        framework.addEventListener(self);
        self.framework = framework;
        sprites = new ArrayList<>();
        dispatcher = new EventDispatcher(sprites);
    }

    public static void main(String[] args) {
        // register for events form your framework
        Framework f = new Framework(); // or window or whatever
        new GameDriver(f).startGame(Integer.parseInt(args[0]));
    }

    void startGame(int players) {
        // initialize game state
        for (int player = 0; player <= players; player++) {
            Sprite s = new Sprite(player);
            sprites.add(s);
            s.start();
        }
        // and your event processing thread
        dispatcher.start();

        // loop forever
        framework.processEvents();
    }

    @Override
    void onEvent(Event e) {
        if (e == Events.Quit) {
            dispatcher.interrupt();
        } eles {
            dispatcher.q.put(e);
        }
    }
}

class EventDispatcher extends Thread implements Runnable {
    // setup a queue for events
    final Queue<Event> q;
    final List<Sprite> sprites;

    EventDispatcher(List<Sprite> sprites) {
        super(this, "Event Dispatcher");
        this.sprites = sprites;
        q = new BlockingQueue<>();
    }

    @Override
    void run() {
        while (!interrupted()) {
            Event e = q.take();
            getSpriteForEvent(e).q.put(e);
        }
        for (Sprite s : sprites) {
            s.interrupt();
        }
    }
}

class Sprite extends Thread implements Runnable {
    final int num;
    final Queue<Event> q;

    Sprite(int num) {
        super(this, "Sprite " + num);
        self.num = num;
        q = new BlockingQueue<>();
    }

    @Override
    void run() {
        while (!interrupted()) {
            Event e = q.take();
            handle(e);
        }
    }

    void handle(Event e) {
        // remember we assumed this takes a really long time..
        // but how do I know how to calculate anything?
        switch (e) {
            case Events.UP:
                   // try to do something really long...
                   waitForUpvoteOn("a/35911559/1254812");
               break; // (;
            ...
        }
    }
}

Now you have new problems to solve. Your game needs a clock. Events need to be batched into time windows which may or may not correlate directly to frames. What happens when one event comes in and a sprite is still processing an old event? Will you cancel processing of the old event or will you drop a frame? You also have to manage the size of your queues--can't produce more events than you can consume.

The point is there has to be a source of truth for determining game state. A referee.. if you will. Turns out it's usually easiest to process all your events on a single thread. Think about it, if each sprite/thread has a ref then they still have to synchronize their individual world views. This effectively serializes the processing of game logic.

Let's add the timer and drawing:

class GameDriver ... {
    static final DELTA = 10; // ms
    final Timer timer;
    ...
    GameDriver(...) {
        ...
        timer = new Timer(dispatcher, DELTA);
        dispatcher = new EventDispatcher(sprites, f.canvas(), map);
    }

    void startGame(...) {
        ...
        // and your event processing thread and timer
        dispatcher.start();
        timer.start();   
        ...
    }

    @Override
    void onEvent(Event e) {
        if (e == Events.Quit) {
            timer.stop();
            dispatcher.interrupt();
        } else {
            if (!dispatcher.q.offer(e)) {
                // Oh no! We're getting more events than we can handle.
                // To avoid getting into this situation you can try to:
                //   1. de-dupe/coalesce/buffer events
                //   2. increase your tick interval (decrease frame rate)
                //   3. drop events (I shot you I swear!)
            }
        }
    }
}

class EventDispatcher ... {
    // setup a queue for events
    final Queue<Event> q;
    final Canvas canvas;

    EventDispatcher(List<Sprite> sprites, Canvas c) {
        super(this, "Event Dispatcher");
        q = new BlockingQueue<>();
        canvas = c;
    }

    @Override
    void tick() {
        for (Sprite s : sprites) {
            canvas.push();
            s.draw(canvas);
            canvas.pop();
        }
    }
}

class Sprite ... implements Drawable ... {
    final Bitmap bitmap;
    final Matrix matrix;
    ...
    Sprite(int num) {
        ...
        URL url = Sprite.class.getResource("sprites/player-"+num+".bmp");
        bitmap = new Bitmap(url);
        matrix = new Matrix();
    }

    @Override
    void draw(Canvas c) {
        c.apply(matrix);
        c.paint(bitmap);
    }

    void handle(Event e) {
        switch (e) {
            case Events.Left:
               matrix.translate(GameDriver.DELTA, 0);
               break;
            case Events.Down:
               matrix.translate(0, GameDriver.DELTA);
            ...
        }
    }
}

And after you cut out the useless threads for each sprite:

class GameDriver ... {
    void startGame(...) {
        // don't need to start() the sprites anymore..
        ...
            // give sprites a starting position
            sprite.matrix.translate(0, player);
    }
}

class EventDispatcher extends Thread implements Runnable {

    final Map<Matrix, Sprite> map;
    ...
    EventDispatcher(...) {
        ...
        map = new HashMap<>();
    }

    ...
    @Override
    void tick() {
        for (Sprite s : sprites) {
            // assuming we gave matrix a map-unique hash function
            checkBounds(s);
            map.put(s.matrix, s);
        }
        // process collisions or otherwise apply game logic
        applyLogic(map);
        map.clear();

        // draw the sprites (or use yet another thread)
        for (Sprite s : sprites) {
            canvas.push();
            s.draw(canvas);
            canvas.pop();
        }
    }

    @Override
    void run() {
        try {
            while (!interrupted()) {
                Event e = q.take();
                getSpriteForEvent(e).handle(e)
            }
        } catch (InterruptedException e) {
        } finally {
            for (Sprite s : sprites) {
                s.interrupt();
            }
        }
    }
    ...
}

class Sprite implements Drawable {
    ...
    // scratch the run method and thread constructor
    ...
}

I don't write games so I probably got something wrong..


Anyway, there are a few takeaways. Recall that your computer has a fixed number of cores. Any number of threads greater than the number of cores will mean you have to switch context between threads. One thread is paused, its registers and stack saved, and a new thread is loaded. This is what your operating system's scheduler does (if it supports threads, as most do).

So an unbounded number of sprites and/or other game objects each backed by it's own thread is just about the worst design you can imagine. It can really drop your tick rate.

Second, as I've already alluded to, in order to avoid race conditions (game checks position of sprite whilst sprite's thread is in the middle of updating it) you have to synchronize access to sprites data, anyway. If you can't compute your logic within one tick on one thread, perhaps you can explore having a work queue to perform sprite updates. But not one thread per sprite.

This is why, as suggested in a comment, 3 threads is a good ballpark. One to interface with the OS. One to process game logic. One to render graphics. (Leaving room for your GC thread if you're using Java.)

Another way to think of it is that your job is to find the smallest window in which you can process input, resolve game state, and emit rendering events for the entire game. Then, just repeat this over and over again. The smaller your window the smoother your game and the higher your frame rate.


Finally, I should mention that the model you seek does actually arise in real world game design but for other reasons. Imagine a multiplayer game with many clients and a server (each effectively representing a new thread of execution). Each client will process its own input events, batch them, translate them to game events, and feed the game events into a server. The network becomes your serialization layer. The server will slurp up events, resolve game state, and send a reply back to the clients. The clients will accept the reply, update their local state, and render it. But the server sure as hell isn't going to wait more than a few frames for slow clients.

Lag sux, my friend.

like image 103
dcow Avatar answered Sep 29 '22 00:09

dcow