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;
}

Sunday, 30 November 2008

The Bridge Class - Part 1

May I apologise in advance to Java experts for (at least) the following irritations they may experience.
  • Due to a lifetime of chasing brace-matches through C code, I cannot seem to take to the Java habit of failing to line up braces vertically;
  • Further, because I've been programming in many languages since 1964 (yes, 1964), I have a habit of calling methods "functions".


Now, let's start to work through the Applet. First, the introductory bit, which specifies the standard packages we will use, and defines the Applet:

// Bridge Applet (c)Amazon Systems

import java.awt.*;
import java.applet.*;
import java.net.URL;
import java.util.Vector;
import java.awt.event.*;
import java.net.MalformedURLException;
public class Bridge extends Applet
implements MouseMotionListener, MouseListener
{

Next, a pile of variables are defined. Many of these are class variables.
Ticktimer is a Thread class handling the timing of the main loop.
BridgeData is where all the Scene data are defined.
DisplayManager and DirtRectSet handle the Graphics.
The variables for communication with Javascript (data and flags) are also defined here.

// Timers
private boolean timerRunning;
public Ticktimer timer = null;
private long timewas, timenow;
public long ticks;
// 1000/n where ticks are 1/n seconds
private static int FRACTION = 1000/12;

// Scene control
public BridgeData bridgedata;
public String currscene;
public String popscene;
public Scene thisscene;
public Image backcloth;
public DirtyRectSet dirtyrectset;
public DisplayManager displaymanager;
public int mousex, mousey;
private Sprite isHolding;
public String codebasestr;

// Communication Area
private boolean invFlag;
private boolean songFlag;
private String songName;
private boolean saveFlag;
private String saveString;
private boolean restoreFlag;
private String restoreData;
private int currCursor;

Saturday, 29 November 2008

The Applet Call

And now, the tiny scrap of HTML that calls the Bridge Applet and handles the buttons.

The first thing the code does is to start the timeout for checking the applet. Once called, this function continuously calls itself again. I fancy that we are sometimes calling an applet that isn't loaded yet. This causes a confusing message from time to time and I should catch and suppress it (using the self.onerror function that is the first line of Javascript) but while in development, I just let the error show. I hoped there ought to be a cleaner method of starting it off, though it's hard for Java to report to its calling function.

Next, we set up a table with the applet load instruction on the left and the buttons on the right.

All the buttons call the applet, either via the javascript functions we talked about yesterday, or directly.

<BODY onLoad = "setTimeout('checkApplet()', 5000);">
<TABLE>
<TR>
<TD>
<APPLET code="Bridge" archive = "com/bridge/bridge11.jar"
width = "640" height="480" id="Bridge"
name="Bridge"> </APPLET></TD>
<TD>
<FORM name = form1>
<input type="button" value="Inventory"
onClick = "document.Bridge.goInv()"><BR>
<input type="button" value="Save"
onClick = "saveGame()"><BR>
<input type="button" value="Restore"
onClick = "restoreGame()"><BR>
<input type="button" value="Switch Music Off"
onClick = "togglesound()" name="togglebutton">
<BR>
</FORM>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>

Friday, 28 November 2008

The Javascript functions in Page3js

(Boy, did I have a job rendering the code in an acceptable way. Finally got it the way it is, and I'm happy with it but I've modified the template for pre tags.)

The page page3js.htm is the page that arrives in the "mainframe" frame.

The first section of this is a couple of functions to deal with the midi songs.

There may be an issue with starting and stopping the music when the applet is "frozen", as the music continues in Firefox if it is on a hidden tab.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE>Bridge - Page3js</TITLE>

<script language="Javascript" type="text/javascript">
//self.onerror = function(){return (true);}


var songname="hush"; //current song
var soundtoggle = "on" //music on


// change the contents of the sound frame
// unless it's already playing
function newsong(name)
{
if (name == songname) return;
if (name=="c12") parent.soundframe.location="c12.htm";
if (name=="ara") parent.soundframe.location="ara.htm";
if (name=="") parent.soundframe.location="hush.htm";

songname = name;
}

// switch music off or on
function togglesound()
{
var temp = songname; //remenber the current song
if (soundtoggle == "on")
{
newsong(""); //silence
songname = temp;
soundtoggle = "off";
document.form1.togglebutton.value="Switch Music On";
}
else
{
songname = "hush";
newsong(temp);
soundtoggle = "on";
document.form1.togglebutton.value="Switch Music Off";
}
}


The following functions deal with communication to and from the applet, which, for historical reasons that I forget, is called Bridge.

The first function calls two more functions that poll the applet for new Save data and for a request to change the song currently being played. Then it arranges to be called again.

The communication always takes the form of a function being called, a function of the form: document.Bridge.function-name(...).

Note that save data is stored in a cookie. On a request from the Save button press, the game is requested to start collecting save data. When a poll from checkSave() results in some data being there, the javascript stuffs it away. When a Restore is requested, the applet is called with the cookie data.

function checkApplet()
{
checkSave();
changeSong();
setTimeout('checkApplet()', 2000);
}
function checkSave()
{
// the term: + "" added to the result to ensure
// against a null return
var savestring = document.Bridge.getSave() + "";
if (savestring != "")
{
var nextyear = new Date();
nextyear.setFullYear(nextyear.getFullYear()+1);
document.cookie = "saved=" + escape(savestring)
+ "; expires=" + nextyear.toGMTString();
}
}
function changeSong()
{
var appsong = document.Bridge.checkSong() + "";
if (appsong != "none")
{
if (soundtoggle == "on")
{
newsong(appsong);
}
else
{
songname = appsong;
}
}
}
function saveGame()
{
document.Bridge.doSave();
}
function restoreGame()
{
var allcookies = document.cookie;
var pos = allcookies.indexOf("saved=");
var start = pos+6;
var end = allcookies.indexOf(";", start);
if (end == -1) end = allcookies.length;
var value = allcookies.substring(start, end);
value = unescape(value);
document.Bridge.doRestore(value);
}
</script>
</HEAD>

Tomorrow I'll continue with the BODY of page3js.htm

Thursday, 27 November 2008

HTML and Javascript Framework


When the user starts up the game, he is calling up an HTML page like the one here. It consists of two frames, one of which you can see, the other is "hidden". We'll come back to the hidden frame in a moment.

The frame you can see has a rectangle where the Java applet resides. The applet is doing most of the work here, but we need to initiate it and communicate with it.


There are also four buttons on a Javascript form which communicate with the Java applet.

Here is the HTML for the outer framework.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE>China 11</TITLE>
</HEAD>
<FRAMESET rows="90%, 1%">
<FRAME SRC="./page3js.htm" name="mainframe">
<FRAME SRC="./hush.htm" name="soundframe">
</FRAMESET>
</HTML>

The almost invisible frame "soundframe" enables the game to play midi tunes. The initial page that goes in that frame is hush.htm, which does nothing:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE>hush</TITLE>
</HEAD>
<BODY>
Bridge hush
</BODY>
</HTML>

For debugging purposes, I can open up the frame and see what page is "playing".

A different page that has a tune on it can be loaded by Javascript at run-time. This is the page for one of the songs.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE>ara</TITLE>
<BGSOUND src="./sounds/arabian.mid" loop = "-1">
</HEAD>
<BODY>
<BGSOUND src="./sounds/arabian.mid" loop = "-1">
<EMBED name = "ara" SRC = "./sounds/arabian.mid" autostart="true"
loop="true" hidden="true" mastersound></embed>
Bridge ara
</BODY>
</HTML>

Note that it uses BGSOUND as well as EMBED. This was to cover old Internet Explorer versions. The idea is that it plays in a loop until the game ends or a new song is put in place. There are a number of problems with this system - not every browser gets the right sound or any sound at all. There's a horrible crunch when the song is changed on Firefox, whereas it all works rather well on IE.

I don't want to dwell too much on this, because I intend to change the mechanism in due course.

Tomorrow I'll start on the main page that sits in the "mainframe" frame and communicates with the applet.

Wednesday, 26 November 2008

Plan of Action

By the way, I'm not going to mess about with extended navel-gazing here. It's my plan to do a top-down analysis of the product to date (bearing in mind its age) and remind myself of how it works as I go.

At the time it was written, typical users didn't have broadband, and some compromises were made to save transfer volumes and improve speeds. This restriction no longer holds.

A link to a zip file containing all the sources can be found in the Links section. Just click the link and my phpzilla download page will come up

Finally, I'll improve it and turn it into the adventure it ought to be, hopefully with some help from my readers.

Introduction


Some five years ago, maybe longer, I started writing an adventure system which could deliver a Myst-like adventure over the web (i.e. through a browser).

I did a proof of concept small adventure which you can see by clicking the image to the right here:

It's a java applet operating with some assistance from a javascript framework.

I caution you... it is pretty lame. There are only 3 or 4 scenes, the click detection is not correct and the music is delivered via a javascript framework.

But it contains the basis for an adventure - the circling birds on the front screen, the ability to pick up and drop the key, the inventory, save and restore features, the ability to add scenes and sprites at will via simple java functions. The scenes are generated using POVRAY.

It began with Mark Tacchi's Gamelet Toolkit, and I'd hoped to use it almost unchanged, but although there are still chunks of Mark's code in the graphics area, my engine is largely unrecognisable.

So this is the starting point. Here we go. Comments and offers of assistance are welcome!