Showing posts with label Alerts. Show all posts
Showing posts with label Alerts. Show all posts

Monday, 9 February 2015

Furniture Sprites

Started implementing Furniture sprites, discovering, incidentally, that if a sprite's image cannot be found, Jade crashes. Must fix that.

Though Furniture sprites are INACTIVE (i.e. don't react to touch or collision), they sometimes have to be moved to and from Limbo, so their 'home' scene and location are remembered. Initially, I set up special variables to remember these, but it occurs to me that I could use a single Dropzone for the purpose. Saves coding.


Tuesday, 18 September 2012

Example Game

For the morbidly interested, I have uploaded the current Alpha version of Jade as Jade.apk, available from http://amazonsystems.phpzilla.net.

This is a "game" with just six scenes, and very little to do except pick up a candle and drop it, but the music and sound effects are there, and the options.

Try it if you dare. Actually it is very benign as alpha versions go. Other than its own running space and private data, it only uses the media player, and surrenders or pauses that whenever it loses focus.

It works for me on my Android Xoom, but I'm interested in the behaviour of other platforms.

From left to right the options are Quit, Restore, Save, Music On/Off, Inventory, Pause.

Double tap to pick up and drop the candle.


Sparing no expense, the source is also available at the same address.
http://amazonsystems.phpzilla.net

Friday, 27 March 2009

Development problem solved

I was finding that new versions of the code were not being acknowledged in Firefox, so that even if I made a change, it kept the old code.

Discovered that you have to bring up the Java console and type x to clear the classloader cache. At first, IE didn't seem to have the same problem, but that was probably because I restarted IE every time I tested it.

Anyway, to be on the safe side, I clear the classloader cache every time now.

And using mousePressed() instead of mouseClicked() works a treat. The mouse response is very much better now.

Development system

Over the last few days, I've set up a simple development system using command line calls to javac etc. I've installed EZ Document Safe to keep the sources and versions in order.

The good news is that the original code still compiles and runs.

I have found out why the mouse click doesn't work so well. If you drag the mouse a little when you press, you don't get a Clicked event; you may or may not get a Dragged event; you may get both; and it all depends on the JRE platform, the timing and how far it was dragged. There's a certain amount of discussion on SunWeb about the matter, and I used a test application to see what was going on.

I've devised a new system which ought to work on all platforms. I'll try it soon.

I have also downloaded JLayer, an mp3 player. It works in free-standing mode, but I don't know how it'll fare when I try to incorporate it into my code. I may just run it in a separate applet like the current midi ones.

Tuesday, 13 January 2009

The Display Manager

Finally, we come to the DisplayManager - the last item in the list of
Java classes.

The DisplayManager handles the graphics, using an off-screen buffer and
a clean version of the background. At each frame change, the previous
dirty rectangles are cleaned by writing the background into them, and
then the sprites are copied to the off-screen buffer, and the new dirty
rectangles redisplayed.

In a paint() call, only the dirty rectangles are displayed. In an
update(), the whole screen is written.

I think an unnecessary DirtyRectSet is defined in Bridge. Priority is
not being taken into account in sprite write sequence. There may still
be some confusion as to when an update and when a full redisplay takes
place.


/**
*
* DisplayManager.java
* @author Mark G. Tacchi (mtacchi@next.com)
* @version 0.8
* Mar 28/1996
* Heavily modified, with permission, by Gil Williamson 2003
*
* DisplayManager provides optimized screen display. Images are drawn to
* an offscreen buffer and blitted out to the display. If images are close
* to one another, they are coalesced and blitted as a single image.
*
* A read only cache is kept which represents an untainted background image.
* This is used in the optimization algorithm as a source for a clean
* background.
*
*/


import java.awt.Rectangle;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MediaTracker;
import java.util.Vector;
import java.awt.Cursor;

public class DisplayManager extends java.lang.Object
{
private Image background;
private Image offScreenBuffer;
private DirtyRectSet dirtyRects = new DirtyRectSet();

/**
* A reference back to the Bridge this manager is servicing.
*/
protected Bridge owner;

public DisplayManager(Bridge theOwner)
{
owner= theOwner;
}

/**
* Set the background image to the specified image.
*/
public void setBackground (Image theImage)
{
background= theImage;
int width = background.getWidth(owner);
int height = background.getHeight(owner);
offScreenBuffer= owner.createImage(width, height);
offScreenBuffer.getGraphics().drawImage (theImage, 0, 0, owner);

dirtyRects.addRect (new Rectangle (0, 0, width,height));

}/*setBackground*/

public void refresh(Graphics g)
{
//gil temp
Rectangle r = new Rectangle(0,0,640,480);
dirtyRects.addRect(r);
paint(g);

}

/**
* Display changed portions of screen.
*/
public void paint(Graphics g)
{
DirtyRectSet flushRects;
Graphics osb;

if( offScreenBuffer == null )
osb = null;
else
osb = offScreenBuffer.getGraphics ();

//
// clear background behind actors...
//
dirtyRects.drawImage (osb, background, owner);

flushRects= dirtyRects;
dirtyRects= new DirtyRectSet();

//
// draw Sprites
//
Vector slist = owner.bridgedata.getCurrentSprites();
for (int ii = 0; ii< slist.size(); ii++)
{
Sprite sprite = (Sprite)slist.elementAt(ii);
// Zilch sprites do not get drawn
if (sprite.getClass() == Zilch.class) continue;
Rectangle r = new Rectangle(sprite.getExtent());
dirtyRects.addRect (r);
flushRects.addRect (r);
Graphics g2= osb.create (r.x, r.y, r.width, r.height);
Image imageii = sprite.getCurrentImage();
g2.drawImage(imageii, 0, 0, owner);
g2.dispose();
}
flushRects.drawImage (g, offScreenBuffer, owner);

} /*paint*/

} /*DisplayManager*/

Monday, 12 January 2009

DirtyRectSet - Dirty Rectangle Set class

DirtyRectSet is the class that collects all the bits of the image that
have been changed and therefore need replacement. It saves processing
time on image update.

Rectangles are added in ascending sequence of x coordinate, and there is
a collapse() method that enables rectangles which are rather close to
each other to be amalgamated. "Close enough" is defined by GLUE - i.e
within GLUE pixels, the rectangles overlap.

The drawImage method is immensely subtle - so subtle that it always
takes me some time to figure out exactly what it's doing. Importantly,
though, it seems to work.

 
/** * * DirtyRectSet.java * Mark Tacchi Mar 15/1996
* * * Slightly modified by Gil Williamson 2003 */

import java.util.Vector;
import java.awt.Rectangle;


public class DirtyRectSet extends java.lang.Object
{
private Vector rects;
public DirtyRectSet()
{
rects = new Vector();
}
public void addRect (Rectangle r)
{
int size = rects.size ();
for (int index = 0; index < size; index++)
{
Rectangle curr = (Rectangle)rects.elementAt (index);

if (r.x > curr.x)
{
rects.insertElementAt (r, index);
return;
}
}
rects.addElement (r);
}

final int GLUE = 64;

final private boolean closeEnough (Rectangle r1, Rectangle r2)
{
boolean result;
r1.width += GLUE;
r1.height += GLUE;
r2.width += GLUE;
r2.height += GLUE;
result = r1.intersects (r2);
r1.width -= GLUE;
r1.height -= GLUE;
r2.width -= GLUE;
r2.height -= GLUE;
return result;
}

public int getSize()
{
return(rects.size());
}

public Rectangle getRect(int index)
{
return ((Rectangle)rects.elementAt(index));
}

public void empty()
{
if (rects.size()>0)
{
rects.removeAllElements();
}
}

public void collapse ()
{
int index = 0;
if (rects.size () < 2)
return;
Rectangle r1 = (Rectangle)rects.elementAt (index);
Rectangle r2 = (Rectangle)rects.elementAt (index+1);
while (true)
{
// collapse R1 and R2
if (closeEnough (r1, r2))
{
r1 = r1.union (r2);
rects.setElementAt (r1, index);
rects.removeElementAt (index+1);
if (index+1 < rects.size ())
r2 = (Rectangle)rects.elementAt(index+1);
else
return;
}
// go to next pair
else if (index+2 < rects.size ())
{
r1 = r2;
r2 = (Rectangle)rects.elementAt (index+2);
index += 1;
}

// done
else
{
return;
}
}
}

public void drawImage (java.awt.Graphics g, java.awt.Image img,
Bridge owner)
{
collapse ();

for (int i= 0; i< rects.size (); i++) {
Rectangle r = (Rectangle)rects.elementAt(i);
java.awt.Graphics g2 = g.create (r.x, r.y, r.width,
r.height);
g2.drawImage(img, -r.x, -r.y, owner);
g2.dispose ();

}
}

}

Saturday, 10 January 2009

The BridgeData class

The BridgeData class varies from game to game. The first part defines all the scenes, zones and sprites. The second part has a number of service routines that deliver the information to calling methods.

There are a couple of method names that I would change for the sake of clarity, specifically:
setCurrentScene() and getCurrentScene(), which really should be called: setCurrentSceneName() and getCurrentSceneName().


// This is the class that defines all the scenes, zones and sprites
import java.util.Vector;
import java.awt.*;
import java.applet.AudioClip;
import java.net.URL;

public class BridgeData extends Object
{
private Vector scenes;
private Vector sprites;
private Scene scene;
private String currscene;
public String startup = "waitintro";
protected Bridge owner;
public BridgeData(Bridge theOwner)
{
owner = theOwner;
scenes = new Vector();
sprites = new Vector();
// Put scenes in here
//inventory
scene = new Scene ("inventory", "inventory.gif");
scene.addZone(20, 20, 80, 80, "", Cursor.CROSSHAIR_CURSOR);
scenes.addElement(scene);
//waitintro
scene = new Scene ("waitintro", "instr.gif");
scene.addZone(238, 176, 164, 124, "intro", Cursor.CROSSHAIR_CURSOR);
scenes.addElement(scene);
//wait
scene = new Scene ("wait", "hourglass.jpg");
scenes.addElement(scene);
//intro
scene = new Scene ("intro", "china.jpg", "c12");
scene.addZone(100, 310, 70, 60, "dock", Cursor.CROSSHAIR_CURSOR);
scenes.addElement(scene);
//dock
scene = new Scene ("dock", "dock.jpg", "ara");
scene.addZone(200, 210, 100, 70, "doornbox", Cursor.CROSSHAIR_CURSOR);
scene.addZone(0, 0, 640, 60, "dockup", Cursor.S_RESIZE_CURSOR);
scene.addZone(0, 0, 60, 480, "dockback", Cursor.E_RESIZE_CURSOR);
scene.addZone(580, 0, 60, 480, "dockback", Cursor.E_RESIZE_CURSOR);
scenes.addElement(scene);
//dockback
scene = new Scene ("dockback", "dockback.jpg", "ara");
scene.addZone(0, 0, 60, 480, "dock", Cursor.E_RESIZE_CURSOR);
scene.addZone(580, 0, 60, 480, "dock", Cursor.E_RESIZE_CURSOR);
scene.addZone(200, 210, 100, 70, "intro", Cursor.CROSSHAIR_CURSOR);
scenes.addElement(scene);
//dockup
scene = new Scene ("dockup", "dockup.jpg", "ara");
scene.addZone(0, 430, 640, 50, "dock", Cursor.S_RESIZE_CURSOR);
scenes.addElement(scene);
//doornbox
scene = new Scene ("doornbox", "doornbox.jpg");
scene.addZone(0, 430, 640, 50, "dock", Cursor.S_RESIZE_CURSOR);
scenes.addElement(scene);
//Put sprites in here
RippleA ripplea = new RippleA("ripplea", "intro");
sprites.addElement(ripplea);
GullA gulla = new GullA("gulla", "intro", 480, 40, 0);
sprites.addElement(gulla);
GullA gullb = new GullA("gullb", "intro", 370, 200, 78);
sprites.addElement(gullb);
GullA gullc = new GullA("gullc", "dockback", 80, 50, 12);
sprites.addElement(gullc);
GullA gulld = new GullA("gulld", "dockback", 150, 200, 78);
sprites.addElement(gulld);
// Put carryables here
GoldKey goldkey = new GoldKey("goldkey", "dock");
sprites.addElement(goldkey);
Zilch dockkeyhole = new Zilch("dockkeyhole", "doornbox");
dockkeyhole.setPosition(200, 240);
URL addr = owner.getCurrentUrl("sounds/rooster.au");
dockkeyhole.addAudio(0, owner.getAudioClip(addr));
sprites.addElement(dockkeyhole);
}
public boolean collide(String spritename, String hitter)
{
if (hitter == null)
{
return true;
}
else
{
if (spritename.equals("dockkeyhole") && (hitter.equals("goldkey")))
{
Sprite found = getSpriteByName(spritename);
if (found != null)
{
AudioClip auclip = found.getAudio(0);
auclip.play();
return true;
}
}
}
return false;
}

// Above here are game-specific functions
// Below here are general purpose functions
public Scene fetchScene(String a)
{
int ii;
if (scenes == null)
{
return null;
}
for(ii = 0; ii < scenes.size(); ii++)
{
scene = (Scene)scenes.elementAt(ii);
if (a.equals(scene.name))
{
return scene;
}
}
return null;
}
public Scene fetchCurrentScene()
{
return (fetchScene(currscene));
}
public void setCurrentScene(String name)
{
currscene = new String(name);
}
public String getCurrentScene()
{
return(currscene);
}
public Vector getSceneSprites(String sceneName)
{
Vector local = new Vector();
Sprite temp = null;
for (int ii = 0; ii < sprites.size(); ii++)
{
temp = (Sprite)sprites.elementAt(ii);
if (temp.scenename.equals(sceneName))
{
local.addElement(temp);
}
}
return local;
}
public Vector getCurrentSprites()
{
return getSceneSprites(currscene);
}
public Vector getAllSprites()
{
return sprites;
}
public Sprite getSpriteByName(String name)
{
Sprite temp;
for (int ii = 0; ii < sprites.size(); ii++)
{
temp = (Sprite)sprites.elementAt(ii);
if (temp.getName().equals(name))
{
return(temp);
}
}
return null;
}
}

Thursday, 8 January 2009

The Scene and Zone classes

The Scene and Zone classes. The comments say it all. The Scene is the equivalent of the "Room" in adventure-speak.
I can see a future need for a method that changes the destination of a zone. It would be of the form - change the destination of the nth zone in the scene which points to scene A to scene B (the normal value of n would be zero - i.e. the only zone).

// Scene class - this is the background to a scene and includes:
// the Zone class - a Zone is a rectangular area within a scene,
// usually for the purpose of enabling the player to click at
// a portion of the scene in order to move to another scene.

import java.util.Vector;
import java.awt.*;
public class Scene extends Object
{
public String name; // Text name of scene
public String imagename; // image file name for scene
public String songname; // text song name
private Vector zones; // list of zones within the scene

// Zone Class
public class Zone extends Object
{
public Rectangle rect; // extent of zone
public String destination; // text name of destination scene
public int cursor; // cursor to display while in zone
public boolean cursoron; // whether cursor displays
public Zone(Rectangle r, String d)
{
rect = new Rectangle(r);
destination = new String(d);
cursoron = false;
}
public Zone(int x, int y, int w, int h, String d, int Cursor)
{
rect = new Rectangle(x, y, w, h);
destination = new String(d);
cursor = Cursor;
cursoron = true;
}
public boolean inZone(int x, int y)
{
if (rect.contains(x, y))
return true;
return false;
}
}
public Scene(String n, String in)
{
name = new String(n);
imagename = new String(in);
zones = new Vector();
songname = new String("");
}
public Scene(String n, String in, String song)
{
name = new String(n);
imagename = new String(in);
zones = new Vector();
songname = new String(song);
}
public void addZone(int x, int y, int w, int h, String d, int e)
{
Zone z = new Zone(x, y, w, h, d, e);
zones.addElement(z);
}
public String getDest(int x, int y)
{
int ii;
Zone tz;
if (zones == null)
return null;
for (ii = 0; ii < zones.size(); ii++)
{
tz = (Zone)zones.elementAt(ii);
if (tz.inZone(x, y))
return tz.destination;
}
return null;
}
public int getZoneCursor(int x, int y)
{
int ii;
Zone tz;
if (zones == null)
return -1;
for (ii = 0; ii < zones.size(); ii++)
{
tz = (Zone)zones.elementAt(ii);
if (tz.inZone(x, y) && tz.cursoron)
return tz.cursor;
}
return -1;
}
}

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 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.

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

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