chapter 12: side scroller - general characteristics the...
TRANSCRIPT
Chapter 12: Side Scroller - General Characteristics
• The background moves past the screen, not the character
• The background and foreground images are usually wider than the screen width
• Background may be layered
– Background consists of multiple images instead of just one
– Give the illusion of depth
– Each layer scrolls at a different rate, ones closer to the front scrolling faster
• Usually implemented using tile maps
– Tiles can be simple bricks
– Can be animated
– ...
• Game perspective is usually a side view, but can be top view, isometric, ...
1
Chapter 12: Side Scroller - Tile Maps
• When implement a scroller, the foreground and background images are gener-ally larger than the display area (usually MUCH larger)
• There are usually multiple levels, each of which has its own set of images
• There are several approaches than can be used for these images
1. Graphics API (OpenGL, java2D) to generate the graphics as needed
– This creates a lot of runtime overhead
– Not flexible - changes require modification to source code
2. Predefined images
– For large images, multiple layers, and many levels, this has high memoryrequirements
– Predefined images make collision detection very problematic
3. Tile map
– Map consists of a grid of tiles
– Each tile is an image
– Consequently, we can generate a large image from a small set imagesstored in memory
– This facilitates collision detection
∗ Can determine if an object will occupy a location in the grid that isalready occupied by a tile
• Components:
1. Configuration file
– This is not required
∗ Map could be hard-coded in program
∗ The configuration file provides flexibility by allowing you to edit thefile without touching the source code
– The file contains
(a) Representation of the map
(b) Usually a link to the tile images
2
Chapter 12: Side Scroller - Tile Maps (2)
– The map representation
∗ Each line represents one row of tiles
∗ Tiles are represented by single characters or character combinations
∗ If using a single character, the number of tiles is limited by the numberof characters (62 if consider only alphanumeric characters)
∗ If using multicharacter representation, use a comma-separated list
44 ,4,4, ,
4 ,4, , ,
, , , ,
1 or , , , ,1
3 , , , ,3
5 2 ,5, , ,2
11111 1,1,1,1,1
2. Internal representation of the tile map
(a) Single image
– When reading the configuration file, the tiles can be written into animage object to create a single image
– This makes scrolling easy, and facilitates modification
– This does not reduce storage or facilitate collision detection
(b) Array of tile images
– Scrolling more difficult, as must determine how much of tiles to dis-play for those that straddle the edges of the display area
– Facilitates collision detection
– Still requires large amount of storage
(c) Indexed tile map
– Tile images are stored in a structure that facilitates retrieval (array,hash table, etc.)
– The map contains references to the images (just like in the configu-ration file)
– Facilitates collision detection
– Reduces storage - only images of individual tiles are maintained
– Increases overhead for scrolling, as must perform lookup for each tilebefore drawing
3
Chapter 12: Side Scroller - Tile Maps (2)
• Creating tile maps
– Easy to make by hand
– Many tile sprites are available on the internet (see resource page)
– Several editors exit for creating tile maps (see resource page)
1. Mappy
∗ Fairly old
∗ Windows-based only
∗ Provides source code to load and display FMP maps for Allegro,OpenGL, DirectX, Java, ...
2. Tiled Map Editor (Tiled Qt)
∗ Windows, OS X, Ubuntu versions
∗ C/C++
∗ A Java version exists, but is not currently being maintained
4
Chapter 12: Side Scroller - Jumping Jack Game Overview
• The title screen shot
• Game play
– Jack must leap over obstacles while avoiding fireballs
– Jack controlled by arrow keys
5
Chapter 12: Side Scroller - Jumping Jack Game Overview (2)
• Components
1. The foreground consists of a tile map
– The map consists of Brick objects
– Each is represented by a GIF
– The bricks are managed by a BricksManager object
2. The background consists of three GIFs
– Each is represented by a Ribbon object
– They are managed by a RibbonsManager object
– These scroll past the JPanel
3. Jack is the player character, implemented by a sprite
– Jack jumps up and down under player control, but has no horizontalmotion
– His legs are animated via a strip image
4. The fireball is a hazard Jack must avoid, implemented by a sprite
– It has lateral velocity, but no associated animation loop
6
Chapter 12: Side Scroller - Jumping Jack Game Overview (3)
• Abbreviated class diagram:
– The framework is the same as used for BugRunner
– These classes (or variants) were discussed in Chapters 2, 3, 4, 5, and 11
7
Chapter 12: Side Scroller - Jumping Jack Game Overview (4)
• Detailed class diagram showing classes new to this game:
• The interesting aspects are involved in the sprite, ribbon, and brick classes
8
Chapter 12: Side Scroller - JumpingJack Class
• Code:
public class JumpingJack extends JFrame implements WindowListener
{
private static int DEFAULT_FPS = 30;
private JackPanel jp;
private MidisLoader midisLoader;
public JumpingJack(long period)
{ super("JumpingJack");
midisLoader = new MidisLoader();
midisLoader.load("jjf", "jumping_jack_flash.mid");
midisLoader.play("jjf", true); // repeatedly play it
Container c = getContentPane(); // default BorderLayout used
jp = new JackPanel(this, period);
c.add(jp, "Center");
addWindowListener( this );
pack();
setResizable(false);
setVisible(true);
}
public void windowActivated(WindowEvent e)
{ jp.resumeGame(); }
public void windowDeactivated(WindowEvent e)
{ jp.pauseGame(); }
public void windowDeiconified(WindowEvent e)
{ jp.resumeGame(); }
public void windowIconified(WindowEvent e)
{ jp.pauseGame(); }
public void windowClosing(WindowEvent e)
{ jp.stopGame();
midisLoader.close(); // not really required
}
public void windowClosed(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
public static void main(String args[])
{
long period = (long) 1000.0/DEFAULT_FPS;
new JumpingJack(period*1000000L); // ms --> nanosecs
}
} // end of JumpingJack class
9
Chapter 12: Side Scroller - JumpingJack Class (2)
• Game speed limited to 30FPS
• Game control managed using WindowListener
• Most movement expressed in terms of moveSize
– Defined in BricksManagerprivate final static double MOVE_FACTOR = 0.25; //BricksManager - for bricks
private int moveSize;
moveSize = (int)(imWidth * MOVE_FACTOR);
private double moveFactors[] = {0.1, 0.5, 1.0}; //RibbonsManager - for ribbons
– Movement rate per update computed in terms of 0.0 ≤ move factor ≤ 1.0and moveSize
∗ 0.0→ standing still
∗ 1.0→ max speed
– The fireball does not have a move size - it moves independently
10
Chapter 12: Side Scroller - JackPanel Class
• Globals:
public class JackPanel extends JPanel implements Runnable, ImagesPlayerWatcher
{
private static final int PWIDTH = 500; // size of panel
private static final int PHEIGHT = 360;
private static final int NO_DELAYS_PER_YIELD = 16;
private static final int MAX_FRAME_SKIPS = 5;
// image, bricks map, clips loader information files
private static final String IMS_INFO = "imsInfo.txt";
private static final String BRICKS_INFO = "bricksInfo.txt";
private static final String SNDS_FILE = "clipsInfo.txt";
private static final String[] exploNames ={"explo1", "explo2", "explo3"};
private static final int MAX_HITS = 20;
private Thread animator; // animation aspects
private volatile boolean running = false;
private volatile boolean isPaused = false;
private long period;
private JumpingJack jackTop;
private ClipsLoader clipsLoader;
// the sprites
private JumperSprite jack;
private FireBallSprite fireball;
// the managers
private RibbonsManager ribsMan;
private BricksManager bricksMan;
private long gameStartTime;
private int timeSpentInGame;
private volatile boolean gameOver = false;
private int score = 0;
private Graphics dbg; // off-screen rendering
private Image dbImage = null;
private boolean showHelp; // to display the title/help screen
private BufferedImage helpIm;
private ImagesPlayer explosionPlayer = null; // explosion-related
private boolean showExplosion = false;
private int explWidth, explHeight; // image dimensions
private int xExpl, yExpl; // coords where image is drawn
private int numHits = 0; // the number of times ’jack’ has been hit
11
Chapter 12: Side Scroller - JackPanel Class (2)
• Constructor:
public JackPanel(JumpingJack jj, long period)
{
jackTop = jj;
this.period = period;
setDoubleBuffered(false);
setBackground(Color.white);
setPreferredSize( new Dimension(PWIDTH, PHEIGHT));
setFocusable(true);
requestFocus(); // the JPanel now has focus, so receives key events
addKeyListener( new KeyAdapter() {
public void keyPressed(KeyEvent e)
{ processKey(e); }
});
// initialise the loaders
ImagesLoader imsLoader = new ImagesLoader(IMS_INFO);
clipsLoader = new ClipsLoader(SNDS_FILE);
// initialise the game entities
bricksMan = new BricksManager(PWIDTH, PHEIGHT, BRICKS_INFO, imsLoader);
int brickMoveSize = bricksMan.getMoveSize();
ribsMan = new RibbonsManager(PWIDTH, PHEIGHT, brickMoveSize, imsLoader);
jack = new JumperSprite(PWIDTH, PHEIGHT, brickMoveSize, bricksMan,
imsLoader, (int)(period/1000000L) ); // in ms
fireball = new FireBallSprite(PWIDTH, PHEIGHT, imsLoader, this, jack);
// prepare the explosion animation
explosionPlayer = new ImagesPlayer("explosion", (int)(period/1000000L),
0.5, false, imsLoader);
BufferedImage explosionIm = imsLoader.getImage("explosion");
explWidth = explosionIm.getWidth();
explHeight = explosionIm.getHeight();
explosionPlayer.setWatcher(this); // report animation’s end back here
// prepare title/help screen
helpIm = imsLoader.getImage("title");
showHelp = true; // show at start-up
isPaused = true;
// set up message font
msgsFont = new Font("SansSerif", Font.BOLD, 24);
metrics = this.getFontMetrics(msgsFont);
}
– brickMoveSize controls amount bricks move per update
– Initially, the help/introduction screen is displayed
∗ This is enabled by setting showHelp and isPaused to true
12
Chapter 12: Side Scroller - JackPanel Class (3)
• Fireball explosion handled here (see animation later)
– Note that fireball sprite not managed here
• gameRender() method (see animation later)
• Game control handled by processKey() via the arrow keys
public void processKey(KeyEvent e)
// handles termination, help, and game-play keys
{
int keyCode = e.getKeyCode();
// termination keys
if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) ||
(keyCode == KeyEvent.VK_END) ||
((keyCode == KeyEvent.VK_C) && e.isControlDown()) )
running = false;
// help controls
if (keyCode == KeyEvent.VK_H) {
if (showHelp) { // help being shown
showHelp = false; // switch off
isPaused = false;
}
else { // help not being shown
showHelp = true; // show it
isPaused = true; // isPaused may already be true
}
}
// game-play keys
if (!isPaused && !gameOver) {
// move the sprite and ribbons based on the arrow key pressed
if (keyCode == KeyEvent.VK_LEFT) {
jack.moveLeft();
bricksMan.moveRight(); // bricks and ribbons move the other way
ribsMan.moveRight();
}
else if (keyCode == KeyEvent.VK_RIGHT) {
jack.moveRight();
bricksMan.moveLeft();
ribsMan.moveLeft();
}
else if (keyCode == KeyEvent.VK_UP)
jack.jump(); // jumping has no effect on the bricks/ribbons
else if (keyCode == KeyEvent.VK_DOWN) {
jack.stayStill();
bricksMan.stayStill();
ribsMan.stayStill();
}
}
}
13
Chapter 12: Side Scroller - JackPanel Class (4)
– Three types of control:
1. Help
∗ H key toggles help screen∗ Game cannot be resumed if help screen displayed
public void resumeGame()
// called when the JFrame is activated / deiconified
{ if (!showHelp)
isPaused = false;
}
2. Play
∗ ← and → control direction
∗ ↑ jumps
∗ ↓ stays still
3. Termination
∗ Triggered by ESC, CTRL-C, END, Q key combinations
– If press two keys in sequence, can have Jack move while jumping, or jumpwhile moving
– To implement multiple key combinations, code must be modified
∗ Use Booleans to remember which keys are currently pressed
∗ Use both keyPressed() and keyReleased() methods in tandem
14
Chapter 12: Side Scroller - JackPanel Class (5)∗ Sample code:
private boolean leftKeyPressed = false;
private boolean rightKeyPressed = false;
private boolean upKeyPressed = false;
public JackPanel(JumpingJack jj, long period)
{
...
addKeyListener(new keyAdapter() {
public void keyPressed(KeyEvent e)
{processKeyPress(e); }
public void keyReleased(KeyEvent e)
{processKeyRelease(e); }
});
...
}
public void processKeyPress(KeyEvent e)
{
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_LEFT) {
leftKeyPressed = true;
else if (keyCode == KeyEvent.VK_RIGHT) {
rightKeyPressed = true;
else if (keyCode == KeyEvent.VK_UP) {
upKeyPressed = true;
if (leftKeyPressed && upKeyPressed)
//up-left action
if (rightKeyPressed && upKeyPressed)
//up-right action
... //other key actions
}
public void processKeyRelease(KeyEvent e)
{
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_LEFT) {
leftKeyPressed = false;
else if (keyCode == KeyEvent.VK_RIGHT) {
rightKeyPressed = false;
else if (keyCode == KeyEvent.VK_UP) {
upKeyPressed = false;
}
}
15
Chapter 12: Side Scroller - JackPanel Class (6)
• run() method and animation
– Mostly unchanged from previous versionspublic void run()
{
long beforeTime, afterTime, timeDiff, sleepTime;
long overSleepTime = 0L;
int noDelays = 0;
long excess = 0L;
gameStartTime = J3DTimer.getValue();
beforeTime = gameStartTime;
running = true;
while(running) {
gameUpdate();
gameRender();
paintScreen();
afterTime = J3DTimer.getValue();
timeDiff = afterTime - beforeTime;
sleepTime = (period - timeDiff) - overSleepTime;
if (sleepTime > 0) { // some time left in this cycle
try {
Thread.sleep(sleepTime/1000000L); // nano -> ms
}
catch(InterruptedException ex){}
overSleepTime = (J3DTimer.getValue() - afterTime) - sleepTime;
}
else { // sleepTime <= 0; the frame took longer than the period
excess -= sleepTime; // store excess time value
overSleepTime = 0L;
if (++noDelays >= NO_DELAYS_PER_YIELD) {
Thread.yield(); // give another thread a chance to run
noDelays = 0;
}
}
beforeTime = J3DTimer.getValue();
int skips = 0;
while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
excess -= period;
gameUpdate(); // update state but don’t render
skips++;
}
}
System.exit(0); // so window disappears
}
16
Chapter 12: Side Scroller - JackPanel Class (7)
– gameUpdate() methodprivate void gameUpdate()
{
if (!isPaused && !gameOver) {
if (jack.willHitBrick()) { // collision checking first
jack.stayStill(); // stop jack and scenery
bricksMan.stayStill();
ribsMan.stayStill();
}
ribsMan.update(); // update background and sprites
bricksMan.update();
jack.updateSprite();
fireball.updateSprite();
if (showExplosion)
explosionPlayer.updateTick(); // update the animation
}
}
∗ Collision checking performed here
· If collision occurs, no update (i.e., no movement)
∗ Explosion updates handled here
17
Chapter 12: Side Scroller - JackPanel Class (8)
– gameRender() methodprivate void gameRender()
{
if (dbImage == null){
dbImage = createImage(PWIDTH, PHEIGHT);
if (dbImage == null) {
System.out.println("dbImage is null");
return;
}
else
dbg = dbImage.getGraphics();
}
dbg.setColor(Color.white);
dbg.fillRect(0, 0, PWIDTH, PHEIGHT);
ribsMan.display(dbg); // the background ribbons
bricksMan.display(dbg); // the bricks
jack.drawSprite(dbg); // the sprites
fireball.drawSprite(dbg);
if (showExplosion) // draw the explosion (in front of jack)
dbg.drawImage(explosionPlayer.getCurrentImage(), xExpl, yExpl, null);
reportStats(dbg);
if (gameOver)
gameOverMessage(dbg);
if (showHelp) // draw the help at the very front (if switched on)
dbg.drawImage(helpIm, (PWIDTH-helpIm.getWidth())/2,
(PHEIGHT-helpIm.getHeight())/2, null);
}
∗ The game elements are drawn back to front
∗ An explosion is drawn if warranted
∗ The help screen is drawn if requested
∗ Ribbon and brick drawing handled by respective managers
18
Chapter 12: Side Scroller - JackPanel Class (9)
– Explosions called by fireball sprite, but handled by JackPanel class (seeabove)public void showExplosion(int x, int y)
// called by fireball sprite when it hits jack at (x,y)
{ if (!showExplosion) { // only allow a single explosion at a time
showExplosion = true;
xExpl = x - explWidth/2; //\ (x,y) is the center of the explosion
yExpl = y - explHeight/2;
/* Play an explosion clip, but cycle through them.
This adds variety, and gets round not being able to
play multiple instances of a clip at the same time. */
clipsLoader.play( exploNames[numHits%exploNames.length], false);
numHits++;
}
}
∗ (x, y) represents the center of the image
· Placement based on upper left corner
· (xExpl, yExpl) computed for this
∗ Concurrent explosions not displayed - only one
∗ Sound clips will play multiple
– sequenceEnded() called at end of explosion looppublic void sequenceEnded(String imageName)
{
showExplosion = false;
explosionPlayer.restartAt(0); // reset animation for next time
if (numHits >= MAX_HITS) {
gameOver = true;
score = (int) ((J3DTimer.getValue() - gameStartTime)/1000000000L);
clipsLoader.play("applause", false);
}
}
∗ Checks to see if game is over
∗ Resets animation for next explosion
∗ Insures that game doesn’t terminate befor eexplosion sequence ended
19
Chapter 12: Side Scroller - RibbonsManager Class
• Code:
public class RibbonsManager
{
private String ribImages[] = {"mountains", "houses", "trees"};
private double moveFactors[] = {0.1, 0.5, 1.0};
private Ribbon[] ribbons;
private int numRibbons;
private int moveSize;
public RibbonsManager(int w, int h, int brickMvSz, ImagesLoader imsLd)
{
moveSize = brickMvSz;
numRibbons = ribImages.length;
ribbons = new Ribbon[numRibbons];
for (int i = 0; i < numRibbons; i++)
ribbons[i] = new Ribbon(w, h, imsLd.getImage( ribImages[i] ),
(int) (moveFactors[i]*moveSize) );
}
public void moveRight()
{ for (int i=0; i < numRibbons; i++)
ribbons[i].moveRight();
}
public void moveLeft()
{ for (int i=0; i < numRibbons; i++)
ribbons[i].moveLeft();
}
public void stayStill()
{ for (int i=0; i < numRibbons; i++)
ribbons[i].stayStill();
}
public void update()
{ for (int i=0; i < numRibbons; i++)
ribbons[i].update();
}
public void display(Graphics g)
{ for (int i=0; i < numRibbons; i++)
ribbons[i].display(g);
}
}
20
Chapter 12: Side Scroller - RibbonsManager Class (2)
• This class handles the backgrounds (ribbons)
• The moveSize variable represents the base movement amount per update
– This is set to the brick move size
– The moveFactors array holds the movement factors for the backgroundlayers
• The background image file names are stored in the ribImages array
• The background images are stored in the ribbons array
• The layers are stored from back to front in the arrays, and must be drawn inthis order
• The manager acts primarily as a router, calling movement, update, and displaymethods for each ribbon
21
Chapter 12: Side Scroller - Ribbon Class
• A ribbon image is preferrably as wide or wider than the panel in which it isdisplayed
– This will require at most two calls to draw()
22
Chapter 12: Side Scroller - Ribbon Class (2)
• Globals and constructor:
public class Ribbon
{
private BufferedImage im;
private int width; // the width of the image (>= pWidth)
private int pWidth, pHeight; // dimensions of display panel
private int moveSize; // size of the image move (in pixels)
private boolean isMovingRight; // movement flags
private boolean isMovingLeft;
private int xImHead;
public Ribbon(int w, int h, BufferedImage im, int moveSz)
{
pWidth = w; pHeight = h;
this.im = im;
width = im.getWidth(); // no need to store the height
if (width < pWidth)
System.out.println("Ribbon width < panel width");
moveSize = moveSz;
isMovingRight = false; // no movement at start
isMovingLeft = false;
xImHead = 0;
}
– xImHead locates left side of image
– isMovingRight/Left are direction flags set by movement methods
• Movement methods:
public void moveRight()
{ isMovingRight = true;
isMovingLeft = false;
}
public void moveLeft()
{ isMovingRight = false;
isMovingLeft = true;
}
public void stayStill()
{ isMovingRight = false;
isMovingLeft = false;
}
23
Chapter 12: Side Scroller - Ribbon Class (3)
• update() method
public void update()
{ if (isMovingRight)
xImHead = (xImHead + moveSize) % width;
else if (isMovingLeft)
xImHead = (xImHead - moveSize) % width;
}
– Adjusts xImHead
−panel width ≤ xImHead ≤ +panel width
• display() method
1. Handles drawing
2. Uses two coordinate systems:
(a) JPanel’s
(b) Image’s
– Both have same height
– These values passed to draw()private void draw(Graphics g, BufferedImage im, int dx1, int dx2, int sx1, int sx2)
{
g.drawImage(im, dx1, 0, dx2, pHeight, sx1, 0, sx2, pHeight, null);
}
– The proper x coordinate in the ribbon is based on xImHead, whichrepresents the location of the left side of the ribbon with respect to thepanel
−width ≤ xImHead ≤ width
24
Chapter 12: Side Scroller - Ribbon Class (4)
3. Five situations to consider:
(a) Case 1: Start up
– The ribbon starts at the left side of the panel
– This also applies to the situation where the ribbon has done a com-plete wrap-around
draw(g, im, 0, pWidth, 0, pWidth);
25
Chapter 12: Side Scroller - Ribbon Class (5)
(b) Case 2: Ribbon moving to right (Jack moving to left)
xImHead < pWidth
– The head and tail meet somewhere within the panel
draw(g, im, 0, xImHead, width-xImHead, width); // im tail
draw(g, im, xImHead, pWidth, 0, pWidth-xImHead); // im head
26
Chapter 12: Side Scroller - Ribbon Class (6)
(c) Case 3: Ribbon moving to right (Jack moving to left)
xImHead ≥ pWidth
– Both the head and tail fall outside the panel
draw(g, im, 0, pWidth, width-xImHead,
width-xImHead+pWidth); // im tail
27
Chapter 12: Side Scroller - Ribbon Class (7)
(d) Case 4: Ribbon moving to left (Jack moving to right)
xImHead ≥ pWidth− width
– Both the head and tail fall outside the panel
– This differs from case 3 because xImHead < 0
draw(g, im, 0, pWidth, -xImHead, pWidth-xImHead); // im body
28
Chapter 12: Side Scroller - Ribbon Class (8)
(e) Case 5: Ribbon moving to left (Jack moving to right)
xImHead < pWidth− width
– Comparable to case 2
draw(g, im, 0, width+xImHead, -xImHead, width); // im tail
draw(g, im, width+xImHead, pWidth, 0,
pWidth-width-xImHead); // im head
29
Chapter 12: Side Scroller - Ribbon Class (9)
• Code:
public void display(Graphics g)
{
if (xImHead == 0) // Case 1: draw im head at (0,0)
draw(g, im, 0, pWidth, 0, pWidth);
else if ((xImHead > 0) && (xImHead < pWidth)) { // Case 2: draw im tail at (0,0) and
// im head at (xImHead,0)
draw(g, im, 0, xImHead, width-xImHead, width); // im tail
draw(g, im, xImHead, pWidth, 0, pWidth-xImHead); // im head
}
else if (xImHead >= pWidth) // Case 3: draw im tail at (0,0)
draw(g, im, 0, pWidth,
width-xImHead, width-xImHead+pWidth); // im tail
else if ((xImHead < 0) && (xImHead >= pWidth-width)) //Case 4
draw(g, im, 0, pWidth, -xImHead, pWidth-xImHead); // im body
else if (xImHead < pWidth-width) { // Case 5: draw im tail at (0,0)
// and im head at (width+xImHead,0)
draw(g, im, 0, width+xImHead, -xImHead, width); // im tail
draw(g, im, width+xImHead, pWidth,
0, pWidth-width-xImHead); // im head
}
}
30
Chapter 12: Side Scroller - BricksManager Class
• The basic operations this class is responsible for:
1. Loading brick info
2. Creating brick structures
3. Moving the brick map
4. Drawing bricks
5. Handling Jack-related interactions
• Globals and constructor:
public class BricksManager
{
private final static String IMAGE_DIR = "Images/";
private final static int MAX_BRICKS_LINES = 15;
private final static double MOVE_FACTOR = 0.25;
private int pWidth, pHeight; // dimensions of display panel
private int width, height; // max dimensions of bricks map
private int imWidth, imHeight; // dimensions of a brick image
private int numCols, numRows;
private int moveSize;
private boolean isMovingRight; // movement flags
private boolean isMovingLeft;
private int xMapHead;
private ArrayList bricksList;
private ArrayList[] columnBricks;
private ImagesLoader imsLoader;
private ArrayList brickImages = null;
public BricksManager(int w, int h, String fnm, ImagesLoader il)
{
pWidth = w;
pHeight = h;
imsLoader = il;
bricksList = new ArrayList();
loadBricksFile(fnm);
initBricksInfo();
createColumns();
moveSize = (int)(imWidth * MOVE_FACTOR);
if (moveSize == 0) {
System.out.println("moveSize cannot be 0, setting it to 1");
moveSize = 1;
}
isMovingRight = false; // no movement at start
isMovingLeft = false;
xMapHead = 0;
}
31
Chapter 12: Side Scroller - BricksManager Class - Loading
• Loading brick info is accomplished by the loadBricksFile() method
• Component info is stored in file bricksInfo.txt
– This stores image data and map data// bricks information
s tiles.gif 5
// -----------
44444
222222222
111
2222
11111
444
444
22222 444 111
1111112222222 23333 2 33 44444444
00 000111333333000000222222233333 333 2222222223333301
00000000011100000000002220000000003300000111111222222234
// -----------
– Image data
∗ The image is a GIF strip tiles.gif
∗ This is read in the usual way
– Map data
∗ The map is represented by lines of digits
∗ Their positions represent where bricks are located, and the images usedfor the bricks
∗ Note that using digits is very limiting - you are limited to only ten stylesof bricks
32
Chapter 12: Side Scroller - BricksManager Class - Loading (2)
• Code:
private void loadBricksFile(String fnm)
{
String imsFNm = IMAGE_DIR + fnm;
System.out.println("Reading bricks file: " + imsFNm);
int numStripImages = -1;
int numBricksLines = 0;
try {
BufferedReader br = new BufferedReader( new FileReader(imsFNm));
String line;
char ch;
while((line = br.readLine()) != null) {
if (line.length() == 0) // ignore a blank line
continue;
if (line.startsWith("//")) // ignore a comment line
continue;
ch = Character.toLowerCase( line.charAt(0) );
if (ch == ’s’) // an images strip
numStripImages = getStripImages(line);
else { // a bricks map line
if (numBricksLines > MAX_BRICKS_LINES)
System.out.println("Max reached, skipping bricks line: " + line);
else if (numStripImages == -1)
System.out.println("No strip image, skipping bricks line: " + line);
else {
storeBricks(line, numBricksLines, numStripImages);
numBricksLines++;
}
}
}
br.close();
}
catch (IOException e)
{ System.out.println("Error reading file: " + imsFNm);
System.exit(1);
}
}
33
Chapter 12: Side Scroller - BricksManager Class - Loading (3)
• A new brick is created for each brick
• Code:
public class Brick
{
private int mapX, mapY; // indicies in the bricks map
private int imageID;
private BufferedImage image;
private int height; // of the brick image
private int locY;
public Brick(int id, int x, int y)
{
mapX = x; mapY = y;
imageID = id;
}
• storeBricks() stores one line of bricks
– The bricks are stored in an ArrayList (bricksList) in no particular order
– Code:private void storeBricks(String line, int lineNo, int numImages)
{
int imageID;
for(int x=0; x < line.length(); x++) {
char ch = line.charAt(x);
if (ch == ’ ’) // ignore a space
continue;
if (Character.isDigit(ch)) {
imageID = ch - ’0’; // we assume a digit is 0-9
if (imageID >= numImages)
System.out.println("Image ID " + imageID + " out of range");
else // make a Brick object
bricksList.add( new Brick(imageID, x, lineNo) );
}
else
System.out.println("Brick char " + ch + " is not a digit");
}
}
34
Chapter 12: Side Scroller - BricksManager Class - Loading (4)
– The following diagram shows the relevant variables associated with the brickmap
35
Chapter 12: Side Scroller - BricksManager Class - Initializing Brick DataStructures
• After bricksList is loaded, initBricksInfo is called
– Its job is to check the integrity of the brick map
1. Wider than panel
2. No gaps in first level
3. Etc.– Code:
private void initBricksInfo()
{
if (brickImages == null) {
System.out.println("No bricks images were loaded");
System.exit(1);
}
if (bricksList.size() == 0) {
System.out.println("No bricks map were loaded");
System.exit(1);
}
BufferedImage im = (BufferedImage) brickImages.get(0);
imWidth = im.getWidth();
imHeight = im.getHeight();
findNumBricks();
calcMapDimensions();
checkForGaps();
addBrickDetails();
}
• createColumns() stores the map on a column-by-column basis
– Each column stored as an ArrayList
– columnBricks is an ArrayList of the columns
– Bricks are not ordered in a column
36
Chapter 12: Side Scroller - BricksManager Class - Initializing Brick DataStructures (2)
– Code:private void createColumns()
{
columnBricks = new ArrayList[numCols];
for (int i=0; i < numCols; i++)
columnBricks[i] = new ArrayList();
Brick b;
for (int j=0; j < bricksList.size(); j++) {
b = (Brick) bricksList.get(j);
columnBricks[ b.getMapX() ].add(b); // bricks not stored in any order
}
}
37
Chapter 12: Side Scroller - BricksManager Class - Moving the Brick Map
• Moving the brick map is performed similarly to ribbons
public void moveRight()
{ isMovingRight = true;
isMovingLeft = false;
}
public void moveLeft()
{ isMovingRight = false;
isMovingLeft = true;
}
public void stayStill()
{ isMovingRight = false;
isMovingLeft = false;
}
public void update()
{
if (isMovingRight)
xMapHead = (xMapHead + moveSize) % width;
else if (isMovingLeft)
xMapHead = (xMapHead - moveSize) % width;
}
• xMapHead represents the left x coordinate of the brick map
−width ≤ xMapHead ≤ +width
38
Chapter 12: Side Scroller - BricksManager Class - Drawing the Brick Map
• There are three coordinate systems that must be contended with:
1. The panel’s
2. The brick map image’s
3. The indices of the bricks relative to the map representation
• The display() method:
public void display(Graphics g)
{
int bCoord = (int)(xMapHead/imWidth) * imWidth;
int offset; // offset is the distance
// between bCoord and xMapHead
if (bCoord >= 0)
offset = xMapHead - bCoord; // offset is positive
else // negative position
offset = bCoord - xMapHead; // offset is positive
if ((bCoord >= 0) && (bCoord < pWidth)) {
drawBricks(g, 0-(imWidth-offset), xMapHead, width-bCoord-imWidth); // bm tail
drawBricks(g, xMapHead, pWidth, 0); // bm start
}
else if (bCoord >= pWidth)
drawBricks(g, 0-(imWidth-offset), pWidth, width-bCoord-imWidth); // bm tail
else if ((bCoord < 0) && (bCoord >= pWidth-width+imWidth))
drawBricks(g, 0-offset, pWidth, -bCoord); // bm tail
else if (bCoord < pWidth-width+imWidth) {
drawBricks(g, 0-offset, width+xMapHead, -bCoord); // bm tail
drawBricks(g, width+xMapHead, pWidth, 0); // bm start
}
}
– xMapHead represents the left x coordinate of the brick map, as noted above
– bCoord represents the x coordinate of the left edge of the brick whose hor-izontal extent includes xMapHead
• The actual drawing is accomplished by drawBricks() (discussed later)
private void drawBricks(Graphics g, int xStart, int xEnd, int xBrick)
– xStart and xEnd represent left and right limits of the active drawing areain the panel
– xBrick represents the coordinate in the brick map that corresponds to xStart
– Bricks are drawn into the panel one column at a time
39
Chapter 12: Side Scroller - BricksManager Class - Drawing the Brick Map (2)
• There are four situations that must be addressed when drawing the brick map
1. Case 1: Map is moving toward the right, bCoord ≤ pWidth
0 ≤ xMapHeight ≤ pWidth
– Requires two calls to drawBricks()
– Complexity due to fact that the left edge of the panel most likely doesnot correspond to the left edge of a brick column
– The first call draws to the left side of the paneldrawBricks(g, 0-(imWidth-offset), xMapHead, width-bCoord-imWidth);
∗ It draws from 0− (imWidth− offset) to xMapHead in the panel
∗ It starts with the brick column at width− bCoord− imWidth– The second call draws to the right side of the panel
drawBricks(g, xMapHead, pWidth, 0);
∗ It draws from xMapHead to pWidth in the panel
∗ It starts with the brick column at 0
40
Chapter 12: Side Scroller - BricksManager Class - Drawing the Brick Map(3)
2. Case 2: Map is moving toward the right, bCoord > pWidth
– Requires a single draw
41
Chapter 12: Side Scroller - BricksManager Class - Drawing the Brick Map(4)
3. Case 3: Map is moving toward the left, bCoord > pWidth − width +imWidth
xMapHead, bCoord < 0
– Requires a single draw
42
Chapter 12: Side Scroller - BricksManager Class - Drawing the Brick Map(5)
4. Case 4: Map is moving toward the left, bCoord < pWidth − width +imWidth
– Requires two draws
• drawBricks() method
private void drawBricks(Graphics g, int xStart, int xEnd, int xBrick)
{
int xMap = xBrick/imWidth; // get the column position of the brick
// in the bricks map
ArrayList column;
Brick b;
for (int x = xStart; x < xEnd; x += imWidth) {
column = columnBricks[ xMap ]; // get the current column
for (int i=0; i < column.size(); i++) { // draw all bricks
b = (Brick) column.get(i);
b.display(g, x); // draw brick b at JPanel posn x
}
xMap++; // examine the next column of bricks
}
}
– xBrick (x coordinate of left edge of starting column of bricks to be drawn)is converted into an index (xMap) into the columnBricks array
– Individual bricks drawn by Brick.display()
– Only the x coordinate is passed since a brick’s y coordinate is fixed
43
Chapter 12: Side Scroller - BricksManager Class - JumperSprite-Related Methods
1. findFloor()
• Jack is initially placed in center of panel at xSprite
• Must determine his y coordinate so he is standing on a brick at that location
• xSprite is converted into a horizontal index into the brick map
• The height of the corresponding column is then calculated• Code:
public int findFloor(int xSprite)
{
int xMap = (int)(xSprite/imWidth); // x map index
int locY = pHeight; // starting y position (the largest possible)
ArrayList column = columnBricks[ xMap ];
Brick b;
for (int i=0; i < column.size(); i++) {
b = (Brick) column.get(i);
if (b.getLocY() < locY)
locY = b.getLocY(); // reduce locY (i.e. move up)
}
return locY;
}
2. Collisions
(a) With bricks moving left or right
• Handled by insideBrick()
• Called before a move is made• Code:
public boolean insideBrick(int xWorld, int yWorld)
{
Point mapCoord = worldToMap(xWorld, yWorld);
ArrayList column = columnBricks[ mapCoord.x ];
Brick b;
for (int i=0; i < column.size(); i++) {
b = (Brick) column.get(i);
if (mapCoord.y == b.getMapY())
return true;
}
return false;
}
44
Chapter 12: Side Scroller - BricksManager Class -JumperSprite-Related Methods (2)
• (xWorld, yWorld) represent the location Jack would be in if the movewere executed
• This coordinate is converted into (x, y) indices into the brick map byworldToMap()
– This returns a Point object in terms of (mapX,mapY )
– Since xWorld can extend beyond the map edges, it is converted tothe range [0, width]
– The y coordinate is adjusted to within the map’s vertical range– Code:
private Point worldToMap(int xWorld, int yWorld)
{
xWorld = xWorld % width; // limit to range (width to -width)
if (xWorld < 0) // make positive
xWorld += width;
int mapX = (int) (xWorld/imWidth); // map x-index
yWorld = yWorld - (pHeight-height); // relative to map
int mapY = (int) (yWorld/imHeight); // map y-index
if (yWorld < 0) // above the top of the bricks
mapY = mapY-1; // match to next ’row’ up
return new Point(mapX, mapY);
}
• The collision test simply checks to see if Jack’s corresponding y index isthe same as a brick’s in this column
• If a collision is detected, no movement takes place
45
Chapter 12: Side Scroller - BricksManager Class - JumperSprite-RelatedMethods (3)
(b) Collisions with the ceiling
• Must check that Jack’s head does not collide with bricks suspended fromthe top of the panel when jumping
• Vertical collision detection handled by checkBrickBase()public int checkBrickBase(int xWorld, int yWorld, int step)
{
if (insideBrick(xWorld, yWorld)) {
int yMapWorld = yWorld - (pHeight-height);
int mapY = (int) (yMapWorld/imHeight); // map y- index
int topOffset = yMapWorld - (mapY * imHeight);
int smallStep = step - (imHeight-topOffset);
return smallStep;
}
return step;
}
• If a collision is imminent, the step vertical distance will need to be ad-justed accordingly
smallStep = step− (imHeight− topOffset)
46
Chapter 12: Side Scroller - BricksManager Class - JumperSprite-RelatedMethods (4)
(c) Landing
• When Jack is falling, must detect when he lands
• This is handled similarly to the way head collisions are detected, but inthe opposite sense• Method checkBrickTop() handles this
public int checkBrickTop(int xWorld, int yWorld, int step)
{
if (insideBrick(xWorld, yWorld)) {
int yMapWorld = yWorld - (pHeight-height);
int mapY = (int) (yMapWorld/imHeight); // map y- index
int topOffset = yMapWorld - (mapY * imHeight);
int smallStep = step - topOffset;
return smallStep;
}
return step; // no change
}
• If a collision is imminent, the step vertical distance will need to be ad-justed accordingly
smallStep = step− topOffset
47
Chapter 12: Side Scroller - Brick Class
public class Brick
{
private int mapX, mapY; // indicies in the bricks map
private int imageID;
private BufferedImage image;
private int height; // of the brick image
private int locY; // the y- coordinate of the brick in the bricks ribbon
public Brick(int id, int x, int y)
{ mapX = x; mapY = y;
imageID = id;
}
public int getMapX()
{ return mapX; }
public int getMapY()
{ return mapY; }
public int getImageID()
{ return imageID; }
public void setImage(BufferedImage im)
{ image = im;
height = im.getHeight();
}
public void setLocY(int pHeight, int maxYBricks)
{ locY = pHeight - ((maxYBricks-mapY) * height); }
public int getLocY()
{ return locY; }
public void display(Graphics g, int xScr)
{ g.drawImage(image, xScr, locY, null); }
}
• Straightforward and simple
• Variations that could be implemented
1. Animated bricks
– Animation would need to be managed by ImagePlayer object(s)
– If used one per brick, would be expensive
– If used a single player to manage all bricks, would result in sameness inanimation
– Could create an animated brick subclass
2. Allow scrolling both horizontally and vertically
– This would require updating bricks’ y coordinates as the character moves
– BricksManager would need to be modified
48
Chapter 12: Side Scroller - FireballSprite Class
• State chart:
49
Chapter 12: Side Scroller - FireballSprite Class (2)
• The constructor calculates a random height for the fireball
• The updateSprite() method does the real work (see following code)
1. Has it collided with jack?
– Checked by hasHitJack()
– To make the collisions more realistic, the bounding rectangle used forcollision detection is a scaled-down version of Jack’s bounding rectangle
2. Has it reached the left edge of the panel?
– Checked by goneOffScreen()
– When it reaches the left edge, it is re-initialized
3. Mapping of the state chart to the code:
(a) move state → updateSprite()
(b) draw state → drawSprite()
50
Chapter 12: Side Scroller - FireballSprite Class (3)
• Code:
public class FireBallSprite extends Sprite
{
private static final int STEP = -10; // moving left
private static final int STEP_OFFSET = 2;
private JackPanel jp; // tell JackPanel about colliding with jack
private JumperSprite jack;
public FireBallSprite(int w, int h, ImagesLoader imsLd, JackPanel jp, JumperSprite j)
{
super( w, h/2, w, h, imsLd, "fireball");
this.jp = jp;
jack = j;
initPosition();
}
private void initPosition()
{
int h = getPHeight()/2 + ((int)(getPHeight() * Math.random())/2);
if (h + getHeight() > getPHeight())
h -= getHeight();
setPosition(getPWidth(), h);
setStep(STEP + getRandRange(STEP_OFFSET), 0); // move left
}
private int getRandRange(int x)
{ return ((int)(2 * x * Math.random())) - x; }
public void updateSprite()
{
hasHitJack();
goneOffScreen();
super.updateSprite();
}
private void hasHitJack()
{
Rectangle jackBox = jack.getMyRectangle();
jackBox.grow(-jackBox.width/3, 0); // make jack’s bounded box thinner
if (jackBox.intersects( getMyRectangle() )) { // jack collision?
jp.showExplosion(locx, locy+getHeight()/2);
initPosition();
}
}
private void goneOffScreen()
{
if (((locx+getWidth()) <= 0) && (dx < 0)) // off left and moving left
initPosition(); // start the ball in a new position
}
}
51
Chapter 12: Side Scroller - JumperSprite Class
• State chart:
52
Chapter 12: Side Scroller - JumperSprite Class (2)
– Once put in horizontal motion, the sprite continues until stopped by theplayer or it collides with a brick
– If it runs off a platform, it will continue its horizontal motion while falling
– There are three concurrent activities:
1. Horizontal motion
2. Vertical motion
3. Updating/drawing
– Rather than use separate threads for each, their implementation is inter-leaved
∗ This imposes a sequence that is not constrained by the state chart
• Constructor:
public class JumperSprite extends Sprite
{
private static double DURATION = 0.5; // secs
private static final int NOT_JUMPING = 0;
private static final int RISING = 1;
private static final int FALLING = 2;
private static final int MAX_UP_STEPS = 8;
private int period; // in ms; the game’s animation period
private boolean isFacingRight, isStill;
private int vertMoveMode;
private int vertStep; // distance to move vertically in one step
private int upCount;
private BricksManager brickMan;
private int moveSize; // obtained from BricksManager
private int xWorld, yWorld;
public JumperSprite(int w, int h, int brickMvSz, BricksManager bm, ImagesLoader imsLd, int p)
{
super(w/2, h/2, w, h, imsLd, "runningRight");
moveSize = brickMvSz;
brickMan = bm;
period = p;
setStep(0,0); // no movement
isFacingRight = true;
isStill = true;
locy = brickMan.findFloor(locx+getWidth()/2)-getHeight();
xWorld = locx; yWorld = locy; // store current position
vertMoveMode = NOT_JUMPING;
vertStep = brickMan.getBrickHeight()/2;
upCount = 0;
}
53
Chapter 12: Side Scroller - JumperSprite Class (3)
– (xWorld, yWorld) represent the sprite’s location
– Initially, Jack is still, facing right
– findFloor() is used to determine his vertical position, as discussed above
– Various variables are initialized
• Mapping of the state chart to the code:
1. Movement states → combined values of Booleans isFacingRight and isStill
2. notJumping, rising, and falling, states → integers 0, 1, 2, respectively
3. initialize state → constructor
4. [willhitbrickabove] state → updateRising()
5. [willhitbrickbelow] state → updateFalling()
6. update() implements transitions from a number of states
54
Chapter 12: Side Scroller - JumperSprite Class (4)
• Motion is handled by the methods moveLeft(), moveRight(), stayStill(), andjump()
– Each motion has a particular image associated with it– Code:
public void moveLeft()
{
setImage("runningLeft");
loopImage(period, DURATION); // cycle through the images
isFacingRight = false; isStill = false;
}
public void moveRight()
{
setImage("runningRight");
loopImage(period, DURATION); // cycle through the images
isFacingRight = true; isStill = false;
}
public void stayStill()
{
stopLooping();
isStill = true;
}
public void jump()
{
if (vertMoveMode == NOT_JUMPING) {
vertMoveMode = RISING;
upCount = 0;
if (isStill) { // only change image if the sprite is ’still’
if (isFacingRight)
setImage("jumpRight");
else
setImage("jumpLeft");
}
}
}
55
Chapter 12: Side Scroller - JumperSprite Class (5)
• Collision detection handled in JackPanel
– willHitBrick() called from within update()
– Horizontal collisions handled by JackPanel so other activities triggered bythe collision can be coordinated
– xTest represents the x position the sprite would be in if it were to be updated
– Vertical collisions handled in JumperSprite because they only affect thesprite
– Code:public boolean willHitBrick()
{
if (isStill)
return false; // can’t hit anything if not moving
int xTest; // for testing the new x- position
if (isFacingRight) // moving right
xTest = xWorld + moveSize;
else // moving left
xTest = xWorld - moveSize;
int xMid = xTest + getWidth()/2;
int yMid = yWorld + (int)(getHeight()*0.8); // use current y posn
return brickMan.insideBrick(xMid,yMid);
}
• updateSprite()
– Updates x coordinate– Code:
public void updateSprite()
{
if (!isStill) { // moving
if (isFacingRight) // moving right
xWorld += moveSize;
else // moving left
xWorld -= moveSize;
if (vertMoveMode == NOT_JUMPING) // if not jumping
checkIfFalling(); // may have moved out into empty space
}
if (vertMoveMode == RISING)
updateRising();
else if (vertMoveMode == FALLING)
updateFalling();
super.updateSprite();
}
56
Chapter 12: Side Scroller - JumperSprite Class (6)
– Calls checkIfFalling()private void checkIfFalling()
{
int yTrans = brickMan.checkBrickTop( xWorld+(getWidth()/2),
yWorld+getHeight()+vertStep, vertStep);
if (yTrans != 0) // yes it could
vertMoveMode = FALLING; // set it to be in falling mode
}
– Conditionally calls updateRising()
∗ Checks if sprite reaches maximum height via BricksManager.checkBrickBase()
∗ Transitions to falling state if it has∗ Code:
private void updateRising()
{ if (upCount == MAX_UP_STEPS) {
vertMoveMode = FALLING; // at top, now start falling
upCount = 0;
}
else {
int yTrans = brickMan.checkBrickBase(xWorld+(getWidth()/2), yWorld-vertStep, vertStep);
if (yTrans == 0) { // hit the base of a brick
vertMoveMode = FALLING; // start falling
upCount = 0;
}
else { // can move upwards another step
translate(0, -yTrans);
yWorld -= yTrans; // update position
upCount++;
}
}
}
– Conditionally calls updateFalling()
∗ Checks to see if has landed via BricksManager.checkBrickTop()∗ Calls finishJumping() if it has
private void finishJumping()
{
vertMoveMode = NOT_JUMPING;
upCount = 0;
if (isStill) { // change to running image, but not looping yet
if (isFacingRight)
setImage("runningRight");
else // facing left
setImage("runningLeft");
}
}
57
Chapter 12: Side Scroller - JumperSprite Class (7)∗ Code:
private void updateFalling()
{
int yTrans = brickMan.checkBrickTop(xWorld+(getWidth()/2), yWorld+getHeight()+vertStep,
vertStep);
if (yTrans == 0) // hit the top of a brick
finishJumping();
else { // can move downwards another step
translate(0, yTrans);
yWorld += yTrans; // update position
}
}
58