Wednesday 31 December 2008

RippleA - rippling water sprite

Here is RippleA, intended to simulate rippling water. It can be seen on the first page of the sample code.

It uses the Sprite.increment() method to change the image at any given time.

It isn't very realistic - perhaps a slower (setCommonTime())

public class RippleA extends Sprite
{
public RippleA(String spriteName, String Scenename)
{
super(spriteName, Scenename, "ripplea.jpg", MULTIPLE,
4, UNMOVING, NOACTION);
setCommonTime(1);
setPosition(0, 360);
setSize(640, 120);
}
public void tick(long ticks, int mousex, int mousey)
{
super.increment(ticks);
}
}

Tuesday 30 December 2008

The Zilch sprite

Zilch is a sprite which is invisible and only serves to detect hits in the part of a scene in which it has been placed. There is an example of this in the original game where the keyhole of the door is covered by a Zilch to enable the audio clip when the gold key hits the keyhole.

public class Zilch extends Sprite
{
public Zilch(String spriteName, String Scenename)
{
super(spriteName, Scenename, "zilch.gif", SINGLE,
1, UNMOVING, NOACTION);
setSize(50,50);
}
}

The Gull sprite


Another sprite, the gull, which is treated differently from a gettable sprite like the key. It moves in a sort of spiral, using its "phase" to decide where it is on its circle, and in choosing a suitable frame. The phase has 96 possible values and deflect is used to determine the x position with respect to start. Meanwhile, the y position just goes up and down in a vertical range. If you look at the sample game, you will see how they smoothly pass over the scenery and even over each other.

There is a certain jerkiness in the animation of the Gull at certain phases - it is not refected in the smooth animation of the animated gif (see right). I think it probably has to do with the calculation of position versus phase and may be improved with a different set of deflections. It is a subject worth revisiting.

Several GullA sprites can coexist; they simply require different start posiitions and phases.

They don't require save and restore code, because they are restored as part of scenery restoration.

import java.awt.Cursor;
import java.awt.Rectangle;
public class GullA extends Sprite
{
int [] deflect = {70, 70, 69, 68, 67, 66, 64, 62, 59,
57, 54, 50, 47, 43, 39, 35, 31, 27, 23, 19, 15, 10, 5, 0};
boolean up;
private int startx, starty, phase;
private final int MAXPHASE = 96;
public GullA(String spriteName, String Scenename, int StartX, int StartY, int Phase)
{
super(spriteName, Scenename, "gull.gif", MULTIPLE,
24, UNMOVING, NOACTION);
startx = StartX;
starty = StartY;
phase = Phase;
setPosition(startx+640, starty);
setSize(28, 27);
up = false;
}
public void tick(long ticks, int mousex, int mousey)
{
int coeff = 0;
increment(ticks);
phase += 1;
if (phase >= MAXPHASE) phase = 0;
setFrame(phase/4);
Rectangle r = getExtent();
if (phase < 24) coeff = deflect[phase];
if ((phase > 23) && (phase < 48))
{
coeff = -deflect[47-phase];
}
if ((phase > 47) && (phase < 72))
{
coeff = -deflect[phase - 48];
}
if ((phase > 71) && (phase < 96))
{
coeff = deflect[95-phase];
}
r.x = startx + coeff;
if (r.y < 28) up = false;
if (r.y > 250) up = true;
if (up)
r.y -= 1;
else
r.y += 1;
setPosition(r.x, r.y);
}
}

The Gold Key sprite


This is an example of a gettable sprite. All it really has is its initialisation, most of which it inherits from the Sprite class, and the save and restore code.

I can't help feeling that a lot of the save and restore stuff could be in the parent class too for movable / gettable sprites.

import java.util.Vector;
import java.awt.Point;
public class GoldKey extends Sprite
{
public GoldKey(String spriteName, String Scenename)
{
super(spriteName, Scenename, "goldk.gif", MULTIPLE,
2, GETTABLE, NOACTION);
setPosition(100, 400);
setSize(40, 20);
addValidDrop("inventory", -1, -1);
addValidDrop("dock", 100, 400);
}
public String saveMe()
{
String mysave = getSavePrefix();
mysave = mysave.concat("[" + scenename + "]");
mysave = mysave.concat(posSave());
mysave = mysave.concat(gotSave());
mysave = mysave.concat(">");
return mysave;
}
public boolean restoreMe(String restoreString)
{
String search = getRestoreContent(restoreString);
int start = search.indexOf("[");
if (start != -1)
{
start +=1;
int end = search.indexOf("]", start);
if (end != -1)
{
scenename = search.substring(start, end);
start = search.indexOf("[X", end);
end = search.indexOf("]", start);
Point p = posRestore(search.substring(start, end+1));
setPosition(p.x, p.y);
if (search.indexOf("[NOTGOT]") != -1)
{
if (dropSprite(scenename))
{
return true;
}
}
else if (search.indexOf("[GOT]") != -1)
{
getSprite();
}
}
}
return false;
}
}

Sunday 28 December 2008

The Sprite Class

Creatively speaking, the sprites (a term stolen from early video games) constitute the interest in the game, against which the backgrounds can be viewed as mere scenery. So I have defined a class, called (unsurprisingly) Sprite, from which all sprites descend. A sprite is an item that is superimposed on the background of a scene. It may be able to be carried by the player, it may move on its own account, like the birds in the example game. It may be a piece of the scenery that can be manipulated by the player. It may be invisible, and have no image, but only an extent. Contact between player cursor and sprite and between sprite and sprite can be detected. A sprite can even be invisible (strictly, transparent). Each sprite is associated with one or more image files to give it one or more representations and/or animations. The parent Sprite class includes a number of variables common to all sprites and a number of default methods that can be overridden in Sprite's descendant classes.
Here is the basic sprite class.

import java.util.Vector;
import java.awt.*;
import java.applet.*;

public class Sprite extends Object
{
// Statics
public static final int UNMOVING = 0; // Never relocates
public static final int MOVING = 1; // Can relocate (move() function)
public static final int GETTABLE = 2; // Can be picked up
public static final int NOACTION = 0; // Unresponsive to mouse
public static final int ACTIVE = 1; // Responsive to mouse (act() function)
public static final int SINGLE = 0; // Single image file (furniture)
public static final int MULTIPLE = 1; // Multiple numbered files
public static final int AGIF = 2; // Animated Gif
public static final int NOANIM = -1; // No Animation


// Static-ish - set up at Constructor
private String spritename;
private String basefile;
private String suffix;
private String currimage;
private int filetype;
private int noframes;
private int movetype;
private int actiontype;
// Parametrable-Static defaulted at Constructor but alterable
private int scale;
private int priority;
private int frametimes[];
private boolean got;
private Vector images;
private Vector audioclips;
// For gettable sprites
public Vector validscenes;
public Vector positions;

// Immediate
public String scenename;
public int cursor;
public boolean cursoron;
private int frame;
private long prevtick;
private Rectangle extent;

// Maximum constructor for a Sprite
public Sprite(String spriteName, String sceneName, String fileName, int fileType,
int noOfFrames, int moveType, int actionType)
{
// text name of sprite
spritename = new String(spriteName);
// text name of 'home' scene
scenename = new String(sceneName);
// text name of image file(s)
basefile = new String(fileName);
int c = basefile.indexOf('.');
suffix = new String(basefile.substring(c));
basefile = new String(basefile.substring(0, c));
// file type single, multiple or animated gif
// (animated gif not yet implemented)
filetype = fileType;
// number of image frames in the sprite set
noframes = noOfFrames;
// movement type (can it relocate?)
movetype = moveType;
// action type (whether responsive to mouse)
actiontype = actionType;
// the images can be scaled for perpective etc.
scale = 100;
// priority determines which sprite is drawn first
priority = 1;
// enables variable speed animation
frametimes = new int[noOfFrames];
// set all the frame times to 'no animation'
setCommonTime(NOANIM);
// x pos, y pos, width, height
extent = new Rectangle(0, 0, 0, 0);
// for special cursor
cursoron = false;
// whether being carried
got = false;
// for Gettable sprites
validscenes = new Vector();
positions = new Vector();
// ?caches for images and audio clips
images = new Vector();
audioclips = new Vector();
// specify which sprite image to use
setFrame(0);
addCursor(Cursor.DEFAULT_CURSOR);
}
public void addAudio(int serial, AudioClip au)
{
audioclips.insertElementAt(au, serial);
}
public AudioClip getAudio(int serial)
{
AudioClip au = (AudioClip)audioclips.elementAt(serial);
return au;
}
public void addImage(int serial, Image im)
{
images.insertElementAt(im, serial);
}
public Image getImage(int serial)
{
Image im = (Image)images.elementAt(serial);
return im;
}
public Image getCurrentImage()
{
Image im = getImage(frame);
return im;
}
public void relocate(String sceneName)
{
scenename = sceneName;
}
// USUALLY OVERRIDDEN EXCEPT FOR NON-ANIMATED GETTABLE ITEMS
public void tick(long ticks, int mousex, int mousey)
{
prevtick = ticks;
if (got)
{
// displace Sprite from carrying cursor
//***perhaps make displacement parametrable***
setPosition(mousex+30, mousey+30);
}
}
// Usual call for animated sprites at tick()
public void increment(long ticks)
{
if ((ticks - prevtick) >= frametimes[frame])
{
setFrame(frame+1);
prevtick = ticks;
}
}
//*** rather suspicious of this ***
public int setPriority(int pr)
{
if (priority > 0)
priority = pr;
return priority;
}
public int setScale(int sc)
{
if ((sc > 0) && (sc <= 100))
scale = sc;
return scale;
}
// Set all the frame times to a single value
public void setCommonTime(int frt)
{
for(int ii = 0; ii < noframes; ii++)
{
frametimes[ii] = frt;
}
}
// Set the position
public void setPosition(int X, int Y)
{
extent.x = X;
extent.y = Y;
}
// Set the size
public void setSize(int X, int Y)
{
extent.width = X;
extent.height = Y;
}
// Get the extent
public Rectangle getExtent()
{
return extent;
}
// Set the current Frame
public void setFrame(int frameNo)
{
if (frameNo < 0)
{
frame = noframes-1;
}
else if (frameNo >= noframes)
{
frame = 0;
}
else
{
frame = frameNo;
}
//*** Not sure this is sensible if we're cacheing images ***
Integer x = new Integer(frame+1000);
String ser = new String(x.toString());
currimage = new String(basefile +
ser.substring(1) + suffix);
}
public int getFrame()
{
return frame;
}
public String getCurrentImageName()
{
return currimage;
}
public String getName()
{
return spritename;
}
public Vector getAllFilenames()
{
Vector vec = new Vector();
for (int ii = 0; ii < noframes; ii++)
{
Integer x = new Integer(ii+1000);
String ser = new String(x.toString());
String sb = new String(basefile +
ser.substring(1) + suffix);
vec.addElement(sb);
}
return vec;
}
public void addCursor(int c)
{
cursor = c;
cursoron = true;
}
public void removeCursor()
{
cursoron = false;
}

// All the Gettable stuff
public void getSprite()
{
if (movetype == GETTABLE)
{
got = true;
//*** Note when a sprite is being carried its image changes to image 1 ***
//*** I don't think there's any check on the range
setFrame(1);
}
}
public boolean isGettable()
{
if (movetype == GETTABLE)
{
return true;
}
return false;
}
public boolean isGot()
{
if (got)
return true;
return false;
}
// Overridden if a click is significant
public boolean hitBy(String hitter)
{
return false;
}
public boolean dropSprite(String Scenename)
{
if (got)
{
for(int ii = 0; ii < validscenes.size(); ii++)
{
String scene =
(String)validscenes.elementAt(ii);
if (scene.equals(Scenename))
{
got = false;
Point point =
(Point)positions.elementAt(ii);
// -1 means anywhere - inventory page etc.
if (point.x != -1)
setPosition(point.x, point.y);
setFrame(0);
return true;
}
}
}
return false;
}
public void addValidDrop(String scene, int xpos, int ypos)
{
validscenes.addElement(scene);
Point point = new Point(xpos, ypos);
positions.addElement(point);
}
public boolean isHit(int hitx, int hity)
{
if (extent.contains(hitx, hity))
{
return true;
}
return false;
}
// overriden for Sprites about which saving/restoring has
// some relevance other than position
public String saveMe()
{
return null;
}
public boolean restoreMe(String savestring)
{
return true;
}
// used by Sprites which saveMe()
public String getSavePrefix()
{
String saver = new String(spritename + "<");
return(saver);
}
public String gotSave()
{
if (this.isGot())
{
return ("[GOT]");
}
else
{
return ("[NOTGOT]");
}
}
//*** I wonder if this causes trouble on restore when it's got ***
//*** There is a situation where a gettable sprite is marooned on restore
public String posSave()
{
Integer X = new Integer(extent.x);
Integer Y = new Integer(extent.y);
String saver = new String("[X" + X.toString()
+ "Y" + Y.toString() + "]");
return saver;
}
public Point posRestore(String restoreString)
{
int X, Y;
X = 0;
Y = 0;
int start = restoreString.indexOf("[X");
int end = restoreString.indexOf("Y", start);
if ((start != -1)&&(end != -1))
{
String temp =
restoreString.substring(start+2, end);
try
{
X = Integer.parseInt(temp);
}
catch (NumberFormatException e)
{
X = 0;
}
start=end + 1;
end = restoreString.indexOf("]", start);
if (end != -1)
{
temp = restoreString.substring(start, end);
try
{
Y = Integer.parseInt(temp);
}
catch (NumberFormatException e)
{
Y = 0;
}
}
}
Point p = new Point(X, Y);
return p;
}
public String getRestoreContent(String restoreString)
{
String search = getSavePrefix();
int start = restoreString.indexOf(search);
if (start != -1)
{
start += search.length();
int end = restoreString.indexOf(">", start);
if (end != -1)
{
search = restoreString.substring(start, end);
return search;
}
}
return null;
}
}

There are definitely some areas for expansion, modification and improvement here, despite the fact that it largely works already. In particular, the areas marked with //*** will reward further study.

Saturday 13 December 2008

The Save / Restore and Mouse actions

Today, we complete Bridge.java with the Save and Restore processes, and the Mouse actions.

Only one save is stored, in a cookie, by the Javascript shell. Whatever state has most recently been saved can be restored as many times as desired.

The static state of the game does not vary much. The state of play is largely determined by the positions of all the sprites. Each sprite is requested to supply its save data.

/* The save process is done in three stages, to prevent
deadly embrace and similar problems with the HTML/Javascript and
the applet.

First, the Javascript sets a saveFlag. Then, when the code is at a good
stage to do the save, the actual save data is built. Finally, every
few seconds, the Javascript checks for a save string in the applet.
Normally this returns an empty string, but in this case it returns
the save data. */

// Called from Javascript
public void doSave()
{
showStatus("doSave"); // gil temp
saveFlag = true;
}
// Called from Javascript
public String getSave()
{
String newsave = new String(saveString);
showStatus("getSave" + newsave); // gil temp
saveString = new String("");
return newsave;
}
// Called from tick()
public String saveGame()
{
String save;
String chunk;
save = new String("game");
save = save.concat("currscene<"+currscene+">");
save = save.concat("popscene<"+popscene+">");
save = save.concat("songname<"+songName+">");
Vector slist = bridgedata.getAllSprites();
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
chunk = sprite.saveMe();
if (chunk != null)
save = save.concat(chunk);
}
return save;
}

In a similar way to the Save process, game restore takes place as a result of a flag set by Javascript and a subsequent detection of that flag in the tick() method.

// Called from Javascript
public void doRestore(String restoredata)
{
showStatus("doRestore" + restoredata); // gil temp
restoreFlag = true;
restoreData = new String(restoredata);
}
// Called from tick()
public boolean restoreGame(String restore)
{
int start, end;
String save;
String chunk;
String restscene;
showStatus("restoreGame" + restore); // gil temp
start = restore.indexOf("currscene<");
end = restore.indexOf(">", start+10);
if ((start == -1)||(end == -1)) return false;
restscene = restore.substring(start+10, end);
start = restore.indexOf("popscene<", end);
end = restore.indexOf(">", start+9);
if ((start == -1)||(end == -1)) return false;
popscene = restore.substring(start+9, end);
start = restore.indexOf("songname<", end);
end = restore.indexOf(">", start+9);
if ((start == -1)||(end == -1)) return false;
songName = restore.substring(start+9, end);
Vector slist = bridgedata.getAllSprites();
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
sprite.restoreMe(restore.substring(end));
if (sprite.isGot())
{
isHolding = sprite;
}
}
currscene = changeScene("wait");
thisscene = bridgedata.fetchCurrentScene();
displaymanager.refresh(getGraphics());
currscene = changeScene(restscene);
thisscene = bridgedata.fetchCurrentScene();
return true;
}

Finally, in the Bridge Applet, we get to the Mouse control area.
There are four basic tasks for a click.
  • If the mouse is clicked in a scene change zone, the scene is changed.
  • If a sprite is being held, and it is clicked on another sprite, then a collision may be detected and acted upon.
  • If a double click is made, a sprite may be taken or dropped.
  • If the click is made on a sprite without a held sprite, that counts as a collision and may result in an action.


// Find out where the mouse is.
public void mouseMoved(MouseEvent e)
{
mousex = e.getX();
mousey = e.getY();
}
// What happens on mouse click
public void mouseClicked(MouseEvent e)
{
Vector slist = bridgedata.getSceneSprites(currscene);
int multi = e.getClickCount();
String dest = thisscene.getDest(e.getX(), e.getY());
// Zone click takes priority
if (dest != null)
{
if (dest.equals(""))
{
if (popscene != null)
{
dest = popscene;
}
}
// Wait process
Scene waitscene = bridgedata.fetchScene("wait");
waitscene.songname = songName;
currscene = changeScene("wait");
thisscene = bridgedata.fetchCurrentScene();
displaymanager.refresh(getGraphics());
currscene = changeScene(dest);
thisscene = bridgedata.fetchCurrentScene();
}
// Click one sprite with another
else if (isHolding != null)
{
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
if (!sprite.getName().equals(isHolding.getName()))
{
if (sprite.isHit(mousex, mousey))
{
bridgedata.collide(sprite.getName(),isHolding.getName());
}
}
}
}

// Pick up sprite
if (multi >= 2)
// Double Click behaviour
{
if (isHolding != null)
{
boolean success = isHolding.dropSprite(currscene);
if (success)
{
isHolding = null;
}
}
else
{
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
if (sprite.isGettable())
{
if (sprite.isHit(mousex, mousey))
{
sprite.getSprite();
isHolding = sprite;
}
}
}
}
}
// Click a sprite when not holding
else
{
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
if (sprite.isHit(mousex, mousey))
{
bridgedata.collide(sprite.getName(),null);
}
}
}
}
public void mouseDragged(MouseEvent e)
{
}
public void mousePressed(MouseEvent e)
{
}
public void mouseReleased(MouseEvent e)
{
}
public void mouseEntered(MouseEvent e)
{
}
public void mouseExited(MouseEvent e)
{
}
}
//End of Bridge

Monday 8 December 2008

The Bridge Applet - Part 3

Now we come to the changeScene function.
  • It makes sure this is a valid scene name;
  • If the player is carrying anything, it relocates the object in the new scene;
  • Displays the hourglass cursor;
  • Fetches the image for the new scene and all the sprite images;
  • Tells bridgedata and displaymanager about it;
  • Flags a song change.

I don't understand the comment which appears to imply that time is saved in subsequent visits to this scene because the data is cached. It looks as though the sprite images are remembered during a scene, but if the same sprite is encountered in a subsequent screen its data is reloaded. Having thought all the way round this, I conclude it's the best strategy. But the way it's implemented is silly, isn't it?

// This function collects the scene and any sprites
// It also reads in the Images for the background and Sprites
// this time only. Once the scene changes, the images are assumed to be
// present.
// Returns the scene name.
public String changeScene(String sceneName)
{
Scene scene = bridgedata.fetchScene(sceneName);
if (scene == null)
{
showStatus("No such scene:" + sceneName);
return currscene;
}
// Deal with any carried items
if (isHolding != null)
{
isHolding.relocate(sceneName);
}
currCursor = Cursor.WAIT_CURSOR;
setCursor(Cursor.getPredefinedCursor(currCursor));
MediaTracker tracker= new java.awt.MediaTracker(this);
URL address = getCurrentUrl("images/"+scene.imagename);
backcloth = getImage(address);
tracker.addImage(backcloth, 0);
Vector slist = bridgedata.getSceneSprites(sceneName);
int serial = 1;
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
Vector sfnames = sprite.getAllFilenames();
for (int jj = 0; jj < sfnames.size(); jj++)
{
String fn = (String)sfnames.elementAt(jj);
URL addr = getCurrentUrl("images/"+ fn);
Image im = getImage(addr);
tracker.addImage(im, serial++);
}
}
try
{
tracker.waitForAll();
}
catch (InterruptedException e)
{
showStatus("Scene Load Media Tracker Interrupted");
}
// cache images with sprite
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
Vector sfnames = sprite.getAllFilenames();
for (int jj = 0; jj < sfnames.size(); jj++)
{
String fn = (String)sfnames.elementAt(jj);
URL addr = getCurrentUrl("images/"+ fn);
Image im = getImage(addr);
sprite.addImage(jj, im);
}
}
currCursor = Cursor.DEFAULT_CURSOR;
setCursor(Cursor.getPredefinedCursor(currCursor));

bridgedata.setCurrentScene(sceneName);
displaymanager.setBackground(backcloth);
songName = scene.songname;
songFlag = true;
return sceneName;
}


Straightforward function to create absolute url from relative url.

// Form url from relative string and base
URL getCurrentUrl(String relative)
{
String urlstr = codebasestr.concat(relative);
try
{
URL urlurl = new URL(urlstr);
return urlurl;
}
catch (MalformedURLException e)
{
showStatus("Error" + urlstr);
}
return null;
}


Here is the function that is called by Javascript when the Inventory button is clicked. It sets a flag to request a scene change to the Inventory.

I remarked earlier that a change to Inventory ought not to take place if the current scene is already Inventory. I could duplicate this test here.

// Called from Javascript
public void goInv()
{
showStatus("goInv"); // gil temp
if (!currscene.equals("inventory")) invFlag = true;
}



// Called from Javascript
public String checkSong()
{
String noSong = new String("none");
showStatus("checkSong"); // gil temp
if (songFlag)
{
songFlag = false;
return(songName);
}
return(noSong);
}

Sunday 7 December 2008

A Little Pause

I've stopped the flow of Java code while I cogitate over a few things, and consult references. I had to fetch all my old Java books out of the garage attic, and one thing led to another.

As a result, it seems that I will implement music via a Java-driven mp3 player thread, rather than the rather lame MIDI implementation in Javascript.

I am also torn about the possibility of cacheing images rather than downloading them every time. This is such a difficult one, I've got to contemplate the consequences.

Wednesday 3 December 2008

Update(), Paint(), tick and the TickTimer Thread

The (Container) method update() may be called, and the default action includes background clearance. We override this to call just the DisplayManager.paint() function.

// Bridge.update() is called when repaint() has been suggested
public void update(Graphics g)
{
displaymanager.paint(g);
}

Another overridden Container method

// Bridge.paint() is called when a complete refresh is necessary
public void paint(Graphics g)
{
displaymanager.refresh(g);
}

This is the tick() function, the heartbeat of the applet. This is aimed to be called every nth of a second, and all the sprites and the cursor are dealt with.

I'm not sure about dodging out altogether if cursor is in WAIT state.

If the current scene is already inventory, we should ignore an inventory request.

The zone cursor selection should take place before the sprite cursor selection, surely.


public void tick()
{
int thisCursor;
// If the current cursor is "hourglass", do nothing
if (currCursor == Cursor.WAIT_CURSOR) return;
// Check for flags from Javascript
if (invFlag)
{
invFlag = false;
popscene = currscene;
currscene = changeScene("inventory");
thisscene = bridgedata.fetchCurrentScene();
}
if (saveFlag)
{
saveFlag = false;
saveString = saveGame();
}
if (restoreFlag)
{
restoreFlag = false;
restoreGame(restoreData);
}
thisCursor = Cursor.DEFAULT_CURSOR;
Vector slist = bridgedata.getCurrentSprites();
for (int ii=0; ii < slist.size(); ii++)
{
// get all the sprites
Sprite sprite = (Sprite)slist.elementAt(ii);
// tick them
sprite.tick(ticks, mousex, mousey);
// cursor them
if (sprite.cursoron)
{
Rectangle r = sprite.getExtent();
if (r.contains(mousex, mousey))
{
thisCursor = sprite.cursor;
}
}
}
int zc = thisscene.getZoneCursor(mousex, mousey);
if (zc != -1)
{
thisCursor = zc;
}
if ((thisCursor != currCursor) && (currCursor != Cursor.WAIT_CURSOR))
{
currCursor = thisCursor;
setCursor(Cursor.getPredefinedCursor(currCursor));
}
repaint();
}

Ticktimer runs as a separate thread, attempting to call tick() n times a second. It uses System time in milliseconds. If it misses any ticks, it figures out how many ticks it has missed and increments the variable ticks accordingly. Some activities, especially sprite movement, care that the ticks have jumped, others just ignore it.

class Ticktimer extends Thread
{
private long interval;
public void run()
{
while (true)
{
timenow = System.currentTimeMillis();
interval = timenow-timewas;
if (interval > FRACTION)
{
interval /= FRACTION;
ticks += interval;
timewas += (interval*FRACTION);
tick();
}
try
{
sleep(((FRACTION-1)>0)?(FRACTION-1):(1));
}
catch(InterruptedException e)
{
return;
}
}
}
}

Monday 1 December 2008

Bridge Part 2 - init() start() stop()

Here's the init() function, called by the browser or applet viewer to inform this applet that it has been loaded into the system.

public void init()
{
// Establish the path (URL) of the applet itself
codebasestr = getCodeBase().toString();
// Clear the area into which Save data will be copied
saveString = new String("");
// Clear the Save flag
saveFlag = false;
// Clear the item that indicates a Sprite is being held by the player
isHolding = null;
// Set up a class instance for the scene data
bridgedata = new BridgeData(this);
// Set up class instances for display stuff
dirtyrectset = new DirtyRectSet();
displaymanager = new DisplayManager(this);
// No music right now
songFlag = false;
// Set the initial scene
currscene = changeScene(bridgedata.startup); // returns scene name
thisscene = bridgedata.fetchCurrentScene(); // returns scene class instance
// Initialise the loop timer
timerRunning = false;
ticks=0;
// Set Mouse interrupt functions
addMouseMotionListener(this);
addMouseListener(this);
// No inventory entry now
invFlag = false;
}

The start() function, called by the browser or applet viewer to inform this applet that it should start its execution. start() and stop() may be called repeatedly during the applet's existence as the page it is on drops out of focus etc. All we do here is to start the timer if it is stopped, and to create a new timer if there isn't one already. This means we can halt operation and restart, effectively freezing the game time. I'm not sure we should call timer.start() unless we have just created it.


public void start()
{
if (!timerRunning)
{
ticks++;
timewas = timenow = System.currentTimeMillis();
timerRunning = true;
if (timer == null)
{
timer = new Ticktimer();
}
}
timer.start();
}

The stop() function, called by the browser or applet viewer to inform this applet that it should stop its execution. It is called when the Web page that contains this applet has been replaced by another page, and also just before the applet is to be destroyed. Apparently it isn't safe to stop() a timer, so we just flag it as suspended. I think we ought to check this flag in the tick() function or in the ticktimer, which we don't currently do, otherwise ticks will just keep going ad infinitum even if we wanted the timer to freeze.

public void stop()
{
timerRunning = false;
}