Session 4: Timers

Create real-time network games in which the server is constantly updating the game world rather than waiting for events.
  1. Have a timer call a method
  2. Write a server
  3. Store lists of objects
  4. Create a class to store data

1. Have a timer call a method

Open up and run the "BasicTimer.java" program in "S4-Timers." When you click on the screen, you will see a dot appear, which is nothing new. But then, the dots move away under their own power, without any mouse or keyboard events to drive them. Clearly, something new has entered into our ObjectDraw world.

That new things is called EventTimer. An EventTimer can be asked to call a method repeatedly on a schedule that you specify. In this case, I told it to "wait one second, then call this FilledOval's move() method every .05 seconds." The parameters passed to move() will be the last two parameters I gave to new EventTimer(), namely, the vx and vy that I just created.

I wanted the direction each oval goes to be random, so I used the special method Math.random(), which returns a random number between 0 and 1. When I multiply that number by 4 and subtract 2, I end up with a number from -2 to 2.

The parameters for making an EventTimer are deliberately ordered in such a way as to mimic making a method call. Note the similarity here:

oval.move(vx, vy);
new EventTimer(oval, "move", vx, vy);

Exercise: Having a timer call your own method
When the ovals in BasicTimer go off the screen, they disappear. Suppose that I wante them to "wrap" around to the other side of the screen. In other words, if the oval's x gets bigger than 600, I set it back to -20; if it gets less than -20, I set it back to 600, and so on. (Why -20 instead of 0?)

In order to do this, I have to move the oval, and then check its position. So, it will not be enough to call any pre-existing method; I need to write my own method to handle this:

public void moveOval(FilledOval oval, double vx, double vy)

Create this method in BasicTimer, and set it up to do as I suggested above. Then, change the EventTimer so that it calls your method instead of oval's move().
Give me a hint?
Your moveOval() method ought to first call oval.move() the same way the timer was calling it before. But then, you'll need to check the oval's position with intstructions like this:

if(oval.getX() < -20) {
    oval.moveTo(600, oval.getY());
}

To write your timer, just think about how you would normally call your new method:

this.moveOval(oval, vx, vy);

How would you translate that into a timer object?
OK, just tell me.
My timer call looks like this:

EventTimer timer = new EventTimer(this, "moveOval", oval, vx, vy);

My moveOval() method looks like this:

public void moveOval(FilledOval oval, double vx, double vy) {
    oval.move(vx, vy);
    
    if(oval.getX() < -20) {
        oval.moveTo(600, oval.getY());
    }
    if(oval.getX() > 600) {
        oval.moveTo(-20, oval.getY());
    }
    if(oval.getY() < -20) {
        oval.moveTo(oval.getX(), 600);
    }
    if(oval.getY() > 600) {
        oval.moveTo(oval.getX(), -20);
    }
}

Here is another option you might prefer:

public void moveOval(FilledOval oval, double vx, double vy) {
    double newX = oval.getX() + vx;
    double newY = oval.getY() + vy;
    
    if(newX < -20) {
        newX = 600;
    }
    if(newX > 600) {
        newX = -20;
    }
    if(newY < -20) {
        newY = 600;
    }
    if(newY > 600) {
        newY = -20;
    }
    oval.moveTo(newX, newY);
}


Exercise: Reading code
The program "NetSpotty.java" contains one example of having a timer simply call an existing method of an object, and another example of having a timer call a customized method. In the latter case, I had to solve the problem of how to have a timer pass itself as a parameter to the method it calls. See if you can tell how I do this, and why it was necessary.
Give me a hint?
Look through the code for places where I create an EventTimer. In one case, it is calling a method of this. Take a look at that method and note which parameter is the timer. Then, see what I used as a parameter in new EventTimer() in order to have it pass itself as one of the parameters.
OK, just tell me.
If you use the special value EventTimer.SELF as one of the parameters to an EventTimer, it will pass itself along in that parameter. In this case, I had to do that because I wanted to stop the timer when the spot had fully faded out. Otherwise, I would eventually build up a very large number of timers all calling a method that no longer had anything to do, and that would slow down the program considerably.


Optional exercise: More fun with timers
The program "Pipes.java" uses EventTimers to run a simulation of water flowing through pipes connecting a number of tanks. By reading through the code you can work out the basics of how it does this. But there are several things that could be improved:To make the second part a bit easier, I've called the manageFloater() method for each floater. This is where you would create and start your timer.
Give me a hint?
The first part of the exercise should be easy: just figure out where I set the pipe's flow rate to 100, and change it to the difference in the two water levels.

The second part is a bit harder. It should be simple enough to write a method that will get the water level in the tank and set the floater to be on top of that water level. However, if you want to make this look good, you'll have to check to see if the floater is below the bottom of the tank, and do something when that happens. It might help to know that all the floaters start on the bottom of their tanks, so you could just get the floater's initial y value and know that it will never have to go below that.

The density parameter is included just in case you want the different floaters to float at different heights in the water; you'll have to think back to your physics classes to work out how to do that.
OK, just tell me.
I changed the setFlowRate() method in two ways: it now checks to make sure that the outlet is below the inlet (greater y value), and it uses the difference in the two water levels as the flow rate:

public void setFlowRate(Pipe pipe, double minLevel) {
    double inletLevel = getWaterLevel(pipe.getInlet(), 0);
    double outletLevel = getWaterLevel(pipe.getOutlet(), canvas.getHeight());
    
    if(inletLevel > minLevel || inletLevel > outletLevel) {
        pipe.setFlowRate(0);
    } else {
        pipe.setFlowRate(outletLevel - inletLevel);
    }
}

My floater code is complicated somewhat by the fact that I tried to account for the floater sitting on the bottom of the tank, or floating in the overflowing background tank:

public void manageFloat(FilledRect floater, Tank tank, double density) {
    new EventTimer(this, "moveFloat", floater, tank, density).start(.02, .02);
}

public void moveFloat(FilledRect floater, Tank tank, double density) {
    double y = tank.getWaterLevel();
    Tank background = tank.getBackground().getTank();
    
    if(background.getWaterLevel() < y) {
        y = background.getWaterLevel();
    }
    y = y - (1 - density) * floater.getHeight();
    
    if(y + floater.getHeight() > tank.getY() + tank.getHeight() - 2) {
        y = tank.getY() + tank.getHeight() - floater.getHeight() - 2;
    }
    floater.moveTo(floater.getX(), y);
}

2. Write a server

You may recall that Network.configure() allows you to specify both a client listener (which has typically been your program itself) and a server listener. So far, we have always been using ServerListener.ECHO, a simple server that just takes any message it receives from one client and broadcasts it to all the clients. This allows us to pretend that the server doesn't exist, since sending a message through any Client gets it to all the other clients. But if we want to have the server be more active in enforcing how the clients act, we'll need to write our own server.

ServerListener, like ClientListener, is an interface, just a list of methods that a class needs to have in order to be used for a particular thing. In this case, there are five events a ServerListener needs to respond to:

public void messageReceived(Server server, int channelID, String channelName, String message)}
public void channelClosed(Server server, int channelID, String channelName)
public void channelOpened(Server server, int channelID, String channelName)
public void serverStarted(Server server)
public void serverStopped(Server server)

The "channels" here are connections to individual clients. The first client to connect will be client 0, the next will be client 1, and so on. Usually client 0 is the player who started the server, assuming that they checked the "Also join as:" box in the "Host network game" dialog.

Take a look at the file ColoredSpottyServer, which is a server that plays a game similar to NetSpotty, but with a different color assigned to each player. When it receives a message, indicating that a particular player clicked at a particular location, it decodes the location, translates it by (-10, -10), and then formulates a reply that consists of the new location, with a semicolon and then the client number added at the end. So, if client number 3 clicks at (60, 281), the server will send "60,281;3" to all the clients.

The encoding and decoding here are done by the methods Code.encodeLocation() and Code.decodeLocation(). The fact that these are methods of the Code class, rather than a particular Code object, means that they are static methods. I've grouped together the encoding and decoding methods in a single class so that we won't have to keep copying those methods into every class we make.

Now take a look at ColoredSpotty. This client is now very simple: when the user clicks, it sends out the location of the click, and when a message is received, it splits off the parts before and after the ";", and makes a color dependent on the client number. The method Color.getHSBColor() is an alternate way of creating a color, by specifying a hue (angle on the "color wheel," with 0 = red, .33 = green, .66 = blue), saturation (how vibrant the color is) and brightness. (There is a fascinating mathematical reason why I get the color angle by multiplying by the golden ratio - the math geeks among you can ask me later if you are interested. It has to do with how sunflower seeds manage to be so closely packed on the flower)

Exercise: Taking turns in Tic-Tac-Toe
Going back to your Tic-Tac-Toe game from the previous session, put a server in place using TicTacToeServer. Have the server keep track of whose turn it is, either channel 0 or channel 1, and only let a player make a move if it is their turn.

Note that you can test your game on your own computer by just starting up the program once, running the server, and then starting another copy of the program, and connecting to your server. To do this, you might want to change the game name to something more distinctive in Network.configure() so that you only see your own server in the Join network game list.
Give me a hint?
Except for changing ServerListener.ECHO to new TicTacToeServer(), you won;t have to make any changes to TicTacToe. Instead, have the server remember in an instance variable what player is net to play, and when the server receives a message, simply ignore it unless it is from that player. If it is from the correct player, forward the message to all the players with server.sendMessage(), and then switch which player is active.
OK, just tell me.
My TicTacToeServer looks like this:

public class TicTacToeServer implements ServerListener {
    int nextPlayer;
    public void messageReceived(Server server, int channelID, String channelName, String message) {
        if(channelID == nextPlayer) {
            server.sendMessage(message);
            nextPlayer = 1 - nextPlayer;
        }
    }
    
    public void serverStarted(Server server) {
        nextPlayer = 0;
    }
}

The methods that I haven't shown here are simply empty. I've used a clever trick to switch channelID between 0 and 1. In general, any time you have an int variable switching between two values, you do not need to use an if-else statement; simply subtract the current value from the sum of the two possible values.


Challenge exercise: Let the player know when it is their turn
The one problem now with our TicTacToe is that a player is always trying to guess, "Am I one of the first two clients? If so, is it my turn, or his?" Let's fix this problem by having the server send a "Your turn." message to the active player.

The server can send a message to a particular player with server.sendMessage("message", 15), where "15" would be the player's channel ID. The client can look for that message like this:

public void messageRecieved(Client client, String message) {
    if(message.equals("Your turn.")) {
        ...
    }
}

You will find this easier to do if you wait for the second player (channel = 1) to log on before notifying one or the other player of whose turn it is. You could even decide who goes first by a random coin toss:

if(Math.random() < .5) {

Give me a hint?
On the server side, you will hve to notify the next player in two separate places: once you have decided the nextPlayer after the second player logs in, and once a player has moved.

On the client side, one simple solution is to create a Text object saying "Your turn.", position it where you want it, and then hide it (text.hide()). When you get the message that it is the player's turn, show the text (text.show()), and then hide it again when the player has clicked.
OK, just tell me.
On the client side, I added a new instance variable Text text, and set it up like this in begin():

text = new Text("Your turn.", 300, 50, Color.GREEN, canvas);
text.setAlignment(Text.CENTER, Text.CENTER);
text.setFontSize(72);
text.hide();

There is a call to text.hide() in onMousePress(), and then messageReceived() looks like this:

public void messageReceived(Client client, String message) {
    if(message.equals("Your turn.")) {
        text.show();
    } else {
        Location point = decodeLocation(message);
        mark(point.getX() / 200, point.getY() / 200);
    }
}

On the server side, I have modified clientConnected() and messageReceived():

public void messageReceived(Server server, int channelID, String channelName, String message) {
    if(channelID == nextPlayer) {
        server.sendMessage(message);
        nextPlayer = 1 - nextPlayer;
        server.sendMessage("Your turn.", nextPlayer);
    }
}

public void clientConnected(Server server, int channelID, String channelName) {
    if(channelID == 1) {
        if(Math.random() < .5) {
            nextPlayer = 0;
        } else {
            nextPlayer = 1;
        }
        server.sendMessage("Your turn.", nextPlayer);
    }
}

3. Store lists of objects

As we start to write "real" network games, the server will be taking over more and more of the job of running the game, and the client will become even simpler than it is now. The advantage of this is that the game is really running just on a single computer, with the other computers having a view of that game and occasionally presenting their events to it to respond to.

In order to have the server running the whole game, it somehow has to know the entire state of the game at all times. So, for example, in order for our TicTacToeServer to really do what we would like, it would have to remember what moves each player had made, in order to be able to determine if a square was still available when someone clicks, and in order to recognize when someone has won or the game has ended in a tie.

We will be using a class called LinkedList in order to store the server's information. A LinkedList can hold any number of a particular type of object, which will be kept in the order I put them in. I can add and remove objects from the start or end of the list, and I can also search through the whole list and do something to each object.

The program TenSpots is an example of using lists. The idea of this program is that there will only be ten spots allowed on the screen at a time. When an eleventh spot is placed, the oldest spot is removed. I do this by keeping a list of all the FilledOval objects I have created, adding new ones at the end of the list, and removing the first one if ever the list gets too big.

Let's look a the code that accomplishes all this. First of all, take a look at how I declare the LinkedList variable, and how I create the LinkedList object:

LinkedList<FilledOval> spots;

public void begin() {
    spots = new LinkedList<FilledOval>();
}

Because this list contains a specific type of object, both the variable declaration and the object creation need to specify not just the list's class (LinkedList) but also the class of object that it contains (FilledOval). LinkedList<FilledOval> is read "A LinkedList of FilledOvals."

Now let's look at where I add and remove list elements, in the onMousePress() method:

public void onMousePress(Location point) {
    point.translate(-10, -10);
    spots.addLast(new FilledOval(point, 20, 20, color, canvas));
    
    if(spots.size() > 10) {
        FilledOval first = spots.removeFirst();
        first.removeFromCanvas();
    }
}

Here, the method call spots.addLast() puts a new object at the end of the list. Then, spots.size() returns the number of objects now in the list. Finally, spots.removeFirst() removes and returns the first object in the list. Sometimes you will want to only look at the first or last item without removing it; for that situation, there are methods getFirst() and getLast() that return the object without changing the list.

The TenSpots program also needs to be able to change the color of all the spots when you press particular keys. Any time that you want to do something to all the objects in a list, you can use a for loop, like the one you see in the changeColor() method:

public void changeColor(Color newColor) {
    for(FilledOval oval : spots) {
        oval.setColor(newColor);
    }
    color = newColor;
}

Whatever is inside the block after the for will be done to each object in the list, where oval will be used to refer to the current object.

You can see more examples of this if you open up the PainterClient and PainterServer classes, which show an example of using lists in a network application. The server, here, tracks all the spots on the screen that are currently painted, and sends out the whole list every tenth of a second. The client, each time it receives a message from the server, clears the canvas and paints the new list of spots. If you look at how the server assembles its message, in its sendState() method, you will notice that it is done with a for loop:

public void sendState() {
    String message = "";
    for(Location spot : paint) {
        message += Code.encodeLocation(spot) + ";";
    }
    
    if(message.length() > 0) network.sendMessage(message);
}

Here, I start out with an empty message, and for every Location in the list, I encode it and add it to the message, with a semicolon after it.

You can see another for loop on the other side, where the client gets the message and has to create a FilledRect for every Location encoded in the message:

public void messageReceived(Client client, String message) {
    canvas.clear();
    
    for(String segment : message.split(";")) {
        Location loc = Code.decodeLocation(segment);
        new FilledRect(loc.getX() * 6, loc.getY() * 6, 6, 6, canvas);
    }
}

Here, message.split(";") creates a list of String objects by breaking apart message at every semicolon. For every one of those Strings, I decode it to get a Location, then figure out where to draw that location on the canvas.

While you are searching through a list in this way, you are not allowed to add or remove any elements, since that might make the loop lose its place in the list. If you want to find and remove a particular element, as I did when I had clicking on a spot of paint remove it in PainterServer, then you have to store the object to be removed in a variable and remove it once you are done with the for. In this case, storing the value in a variable also made it possible for me to remember whether I found something to remove:

public boolean isOpen(Location loc) {
    Location found = null;
    
    for(Location spot : paint) {
        if(spot.getX() == loc.getX() && spot.getY() == loc.getY()) {
            found = spot;
        }
    }
    
    if(found != null) {
        paint.remove(found);
        return false;
    } else {
        return true;
    }
}

If there were a possibility that I might need to remove more than one object, I could store the objects to be removed in a list, and then remove them all at once, after the loop, with list.removeAll(). You can see an example of this in SpaceWarsServer.

4. Create a class to store data

In the Painter program, I found that the Location class served nicely for the information that I needed to store. More often, however, you will want to write your own class to represent the data the server is working with.

You can see an example of this in the "snakes" game. The server has a list of four Snake objects, where Snake is a class that i designed just for that game. In addition to a list of segments, the Snake knows whether it is growing or dying, and what direction it is moving in. Writing my own class also let me group together the methods that operate on the snake in some way - moving it, checking for collisions, and encoding its state to send over the network. This vastly simplified the server, since most of the rules for how a snake moves were kept in the Snake class.

You can see another example of this in SpaceWarsGame. Here, I made a class called Particle that represents one of the objects on the screen, specifying not just the position but also the velocity, size, and what Client it belongs to. Then, I made a subclass of Particle to represent the Ship, so that Ship would inherit all the basic behavior of a Particle but add on some features of its own.

Designing your own classes is a much too complicated topic to really get into now; it is at the heart of what real programmers do. But hopefully, this seminar has given you something of a taste of what it looks and feels like to write computer programs, and if I've managed to spark your interest, you can look around in you library and on the internet and learn more on your own.

back to Creating Network Games with ObjectDraw