Thursday 29 January 2009

A Little Pause

Some cretin hacked into one of my other sites, and installed a php bulk mailer, presumably in the laudable aim of extending the world's reproductive capabilities. Since I believe that the world is already over-populated, I made haste to trash the whole thing, and I'm rebuilding it. Back to game development soon.

Tuesday 13 January 2009

The Graphic Environment of the Game


Using PovRay, a free ray-tracing package, I've built the island environment in which the game plays. You only see a fraction of it in the sample game, and even that is badly textured.

The basic terrain was mapped out using GFORGE - producing a height model which PovRay accepts and builds into a surface.

The scale is not vast. At the left of the plan above, you can see the L-shaped landing stage in brown, which is 2.8 metres wide.

A bonus of this construction method is that the interior of the surface is hollow, and so if a tunnel is driven into the rock, it reveals a cavern, the full height of the mountain. It is necessary to conceal the fact that the mountain is paper-thin by never showing the actual edge of the hole!

The development plans I have so far are :
  • a "trophy room" type of ending to the game, in which, once all the collected items are brought to the room, the game ends
  • a door with a maze lock
  • an elevator worked by water power which the player has to set working by moving valves
  • boats
  • bamboo stairways and structures
  • trees and vegetation
  • lots of odd items
  • a control centre on the left hand peak which lifts and lowers bridges and generally opens and closes pathways.
I intend to concentrate on a rich environment that is a pleasure to be in, as opposed to a simple puzzle.

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