coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
on:
2011-10-10 19:27:34 » |
|
Hey, guys! I'm new to this forum and I'd like to say first that I was amused by the account activation. I decided to join here because it seems like the most likely place I can finally find the solution to a problem I've been having for the last week... I've tried help from GameDev and from the Oracle forums over the last few days, but I haven't received the solution to it yet. Basically, what I have is a tile engine that I've constructed for a side-scrolling game I'm developing. It reads in a .png image that gets converted to an array representing the level's tiles, which then is used to calculate the view that is drawn to the screen. The user can move the little character in the middle of the screen (which right now there is no collision detection, so he/she can move the player about in any direction regardless of what tile). The problem is that the thing is super slow! Originally, I found that I was redrawing the view every time rather than shifting it, which was thought to have been the bottleneck, but I converted it to draw the view only when the player has moved to where a new view has to be calculated and rerendered, and the slowness is still there. My computer is a beast gaming machine, so I'm not thinking it's computer performance related at all, but I must have messed up somewhere. I'll post the code for the classes that compose the tile engine below. I would greatly appreciate any and all help and/or criticism and/or advice pertaining to the tile engine that anyone is willing to offer. :] I apologize that this post will be so lengthy, but since there are several components to the engine rather than just one class, I don't want to leave a stone unturned for anybody. Thanks again! :] This class represents the Player and manages his current state of animation: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
| import java.awt.image.*; import java.io.*; import javax.imageio.*; import java.awt.*; public class Player { private final int TILE_SIZE = 32; private final int SCREEN_WIDTH = 1280; private final int SCREEN_HEIGHT = 768; private BufferedImage playerSheet; private BufferedImage playerFrames[]; private int currentIndex = 0; private enum PlayerState {STILL_LEFT, STILL_RIGHT, MOVE_LEFT, MOVE_RIGHT, JUMP} private PlayerState currentState; private PlayerState lastState; private long movementTimer; private long stillTimer; public Player() { playerSheet = makeColorTransparent("playersheet1.png", 0xFFFF65F6); playerFrames = splitImage(playerSheet, 4, 2); movementTimer = System.nanoTime(); stillTimer = movementTimer; currentState = PlayerState.STILL_LEFT; lastState = PlayerState.STILL_LEFT; } public void drawPlayer(Graphics gr, int x, int y) { gr.drawImage(playerFrames[currentIndex], x, y, null); } private static BufferedImage loadImage(String ref) { BufferedImage bimg = null; try { bimg = ImageIO.read(new File(ref)); } catch (Exception e) { e.printStackTrace(); } return bimg; } private BufferedImage[] splitImage(BufferedImage img, int cols, int rows) { int w = img.getWidth() / cols; int h = img.getHeight() / rows; int num = 0; BufferedImage imgs[] = new BufferedImage[w * h]; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { imgs[num] = new BufferedImage(w, h, img.getType()); Graphics2D g = imgs[num].createGraphics(); g.drawImage(img, 0, 0, w, h, w * x, h * y, w * x + w, h * y + h, null); g.dispose(); num++; } } return imgs; } public static BufferedImage makeColorTransparent(String ref, int color) { BufferedImage image = loadImage(ref); BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = dimg.createGraphics(); g.setComposite(AlphaComposite.Src); g.drawImage(image, null, 0, 0); g.dispose(); for (int i = 0; i < dimg.getHeight(); i++) { for (int j = 0; j < dimg.getWidth(); j++) { if (dimg.getRGB(j, i) == color) { dimg.setRGB(j, i, 0x8F1C1C); } } } return dimg; } public void updatePlayer(int input) { stillTimer = System.nanoTime(); switch (input) { case 0: break; case 1: if (currentState != PlayerState.MOVE_LEFT) { movementTimer = System.nanoTime(); } currentState = PlayerState.MOVE_LEFT; lastState = PlayerState.MOVE_LEFT; break; case 2: if (currentState != PlayerState.MOVE_RIGHT) { movementTimer = System.nanoTime(); } currentState = PlayerState.MOVE_RIGHT; lastState = PlayerState.MOVE_RIGHT; break; case 3: break; case 4: break; case 5: currentState = PlayerState.STILL_LEFT; lastState = PlayerState.STILL_LEFT; break; case 6: currentState = PlayerState.STILL_RIGHT; lastState = PlayerState.STILL_RIGHT; break; } } public void animatePlayer() { switch (currentState) { case STILL_LEFT: currentIndex = 0; break; case STILL_RIGHT: currentIndex = 4; break; case MOVE_LEFT: if (currentIndex == 0 || currentIndex == 4) { currentIndex = 1; } if (System.nanoTime() - movementTimer > 100000000) { if (currentIndex == 1) { currentIndex = 2; } else { currentIndex = 1; } movementTimer = System.nanoTime(); } break; case MOVE_RIGHT: if (currentIndex == 0 || currentIndex == 4) { currentIndex = 5; } if (System.nanoTime() - movementTimer > 100000000) { if (currentIndex == 5) { currentIndex = 6; } else { currentIndex = 5; } movementTimer = System.nanoTime(); } break; case JUMP: break; } } }
|
This class represents the Level object that stores the .png images that will compose the drawn level and the array that represents the tiles in a more memory-efficient manner. It also manages updating the level's view and rendering it, as well as drawing it to the screen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
|
import java.awt.image.*; import java.io.*; import javax.imageio.*; import java.awt.*;
public class Level { private final int TILE_SIZE = 32; private final int SCREEN_WIDTH = 1280; private final int SCREEN_HEIGHT = 768; private BufferedImage levelImage; private int width, height; private String levelName; private LevelCollisions myCollisions; private int levelTiles[][]; private BufferedImage tileSheet; private BufferedImage[] tiles; private BufferedImage cameraImage; private BufferedImage sideBufferL, sideBufferR, sideBufferT, sideBufferB; private Graphics cameraG; private int offsetX, offsetY; private int coordX, coordY; private static final int SPACE_COLOR = 0xFF000000; private static final int WALL_COLOR = 0xFFFFFFFF; public Level(String level) { levelName = level; levelImage = loadImage(level + ".png"); myCollisions = new LevelCollisions(level + "Collision"); levelTiles = loadLevel(); cameraImage = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB); cameraImage.createGraphics(); cameraG = cameraImage.getGraphics(); offsetX = offsetY = 0; coordX = 600; coordY = 383; tileSheet = loadImage("obstacletiles.png"); tiles = splitImage(tileSheet, 2, 1); this.renderLevel(); } public int[][] loadLevel() { height = levelImage.getHeight(); width = levelImage.getWidth(); int levelValues[][] = new int[width][height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { levelValues[x][y] = levelImage.getRGB(x, y); } } return levelValues; } public int getTile(int x, int y) { return levelTiles[x][y]; } public void drawLevel(Graphics gr, int x, int y) { gr.drawImage(cameraImage, x + offsetX, y + offsetY, null); } public void renderLevel() { int x, y; int tileX, tileY; tileY = coordY; for (y = 0; y < SCREEN_HEIGHT; y += TILE_SIZE) { tileX = coordX; for (x = 0; x < SCREEN_WIDTH; x += TILE_SIZE) { switch (this.getTile(tileX, tileY)) { case SPACE_COLOR: cameraG.drawImage(tiles[0], x, y, null); break; case WALL_COLOR: cameraG.drawImage(tiles[1], x, y, null); break; } tileX++; } tileY++; } if (offsetX > 0) { } if (offsetX < 0) { } if (offsetY < 0) { } if (offsetY > 0) { } } public void updateLevel(int input) { switch (input) { case 0: if (coordY > 30) { offsetY += 2; } if (offsetY >= TILE_SIZE) { offsetY = 0; coordY--; this.renderLevel(); } break; case 1: if (coordX > 30) { offsetX += 2; } if (offsetX >= TILE_SIZE) { offsetX = 0; coordX--; this.renderLevel(); } break; case 2: if (coordX < width - 30) { offsetX -= 2; } if (offsetX <= -TILE_SIZE) { offsetX = 0; coordX++; this.renderLevel(); } break; case 3: if (coordY < height - 30) { offsetY -= 2; } if (offsetY <= -TILE_SIZE) { offsetY = 0; coordY++; this.renderLevel(); } break; case 4: break; } } public static BufferedImage loadImage(String ref) { BufferedImage bimg = null; try { bimg = ImageIO.read(new File(ref)); } catch (Exception e) { e.printStackTrace(); } return bimg; } public static BufferedImage[] splitImage(BufferedImage img, int cols, int rows) { int w = img.getWidth() / cols; int h = img.getHeight() / rows; int num = 0; BufferedImage imgs[] = new BufferedImage[w * h]; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { imgs[num] = new BufferedImage(w, h, img.getType()); Graphics2D g = imgs[num].createGraphics(); g.drawImage(img, 0, 0, w, h, w * x, h * y, w * x + w, h * y + h, null); g.dispose(); num++; } } return imgs; } public static BufferedImage makeColorTransparent(String ref, int color) { BufferedImage image = loadImage(ref); BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = dimg.createGraphics(); g.setComposite(AlphaComposite.Src); g.drawImage(image, null, 0, 0); g.dispose(); for (int i = 0; i < dimg.getHeight(); i++) { for (int j = 0; j < dimg.getWidth(); j++) { if (dimg.getRGB(j, i) == color) { dimg.setRGB(j, i, 0x8F1C1C); } } } return dimg; } }
|
This is the code for the renderer itself, the class I called LevelRenderer. It calls all of the methods that perform the drawing and updating: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
| import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import javax.imageio.*; import javax.swing.JFrame; import javax.swing.*; import java.util.Random; import java.awt.Color;
public class LevelRenderer extends JFrame { private final int TILE_SIZE = 32; private final int SCREEN_WIDTH = 1280; private final int SCREEN_HEIGHT = 768; private BufferedImage buffer2; private BufferStrategy buffer; private Player myPlayer; private Level myLevel; private Screen s; private Graphics gr; private Graphics graphics; private boolean endGame; public LevelRenderer() { setPreferredSize(new Dimension(1280, 768)); setIgnoreRepaint( true ); setUndecorated( true );
setFocusable(true); requestFocus(); setResizable(false); addKeyListener( new KeyAdapter() { public void keyPressed(KeyEvent e) { processKey(e); } public void keyReleased(KeyEvent e) { processRelease(e); } }); buffer2 = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB); buffer2.createGraphics(); gr = buffer2.getGraphics(); myPlayer = new Player(); myLevel = new Level("obstaclemap"); endGame = false; } public static BufferedImage loadImage(String ref) { BufferedImage bimg = null; try { bimg = ImageIO.read(new File(ref)); } catch (Exception e) { e.printStackTrace(); } return bimg; } public void run(DisplayMode dm) { setBackground(Color.WHITE); s = new Screen(); s.setFullScreen(dm, this); this.createBufferStrategy( 2 ); buffer = this.getBufferStrategy();
while (!endGame) { try { myPlayer.animatePlayer(); myLevel.drawLevel(gr, 0, 0); myPlayer.drawPlayer(gr, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE); graphics = buffer.getDrawGraphics(); graphics.drawImage(buffer2, 0, 0, null); if( !buffer.contentsLost() ) { buffer.show(); } } catch (Exception ex) { System.err.println("Game Update Error: " + ex); } try { Thread.sleep(10); } catch (Exception ex) { System.out.println("Can't sleep!"); } } s.restoreScreen(); } public void processKey(KeyEvent e) { int keyCode = e.getKeyCode(); boolean moved = false; int xDisplace, yDisplace; if (keyCode == KeyEvent.VK_ESCAPE) { endGame = true; } switch (keyCode) { case KeyEvent.VK_UP: myPlayer.updatePlayer(0); myLevel.updateLevel(0); break; case KeyEvent.VK_LEFT: myPlayer.updatePlayer(1); myLevel.updateLevel(1); break; case KeyEvent.VK_RIGHT: myPlayer.updatePlayer(2); myLevel.updateLevel(2); break; case KeyEvent.VK_DOWN: myPlayer.updatePlayer(3); myLevel.updateLevel(3); break; case KeyEvent.VK_SPACE: myPlayer.updatePlayer(4); myLevel.updateLevel(4); break; } } public void processRelease(KeyEvent e) { int keyCode = e.getKeyCode(); boolean moved = false; int xDisplace, yDisplace; switch (keyCode) { case KeyEvent.VK_UP: myPlayer.updatePlayer(0); break; case KeyEvent.VK_LEFT: myPlayer.updatePlayer(5); break; case KeyEvent.VK_RIGHT: myPlayer.updatePlayer(6); break; case KeyEvent.VK_DOWN: myPlayer.updatePlayer(3); break; case KeyEvent.VK_SPACE: myPlayer.updatePlayer(4); break; } } }
|
Lastly, this is the file that just has the main loop in it to call the other classes: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
import javax.swing.JFrame; import javax.swing.*; import java.awt.*;
public class GameTest { public static void main(String args[]) { LevelRenderer myRenderer = new LevelRenderer(); DisplayMode dm = new DisplayMode(1280, 768, 16, DisplayMode.REFRESH_RATE_UNKNOWN); myRenderer.run(dm); } }
|
Once again, I apologize for posting so much, especially as a first post. I really hope you guys will be able to assist me in locating the problem! :] Thanks so much! Edit: Also, I put the application through the Profiler in Netbeans and it seems that the drawLevel() method is incurring a lot of overhead, as is the run method. However, I'm still not sure as to how to go about fixing this. Best regards, Colton
|
|
|
|
|
ra4king
JGO Kernel      Posts: 3160 Medals: 196
I'm the King!
|
 |
«
Reply #1 on:
2011-10-10 23:39:55 » |
|
The few improvements you can make: - You don't need to draw into a buffer and then draw that buffer to the screen. Since you are using BufferStrategy, it draws it into a back buffer for you until you call show(). - Follow BufferStrategy's javadoc's instructions on how to correctly put the drawing code in while loops. - You are drawing a 1280x768 image in drawLevel()!! That might be a big cause of the slowdown. EDIT: Aha, I found out why the code looks familiar: been watching Bucky lately? 
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #2 on:
2011-10-10 23:43:44 » |
|
If you post the complete source as a .rar uploaded to somewhere I can try it out on my computer. It's kind of time consuming to catch the flow of a program by just looking at the code. But do listen to Ra4king's advice first and see if it solves the problem. I'm no Java2D expert.
|
There is no god.
|
|
|
Games published by our own members! Go get 'em!
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #3 on:
2011-10-11 00:19:55 » |
|
Thanks for the responses, guys! ra4king: I made the changes you suggested. However, it did not make the program any faster. Is there a way I can restructure or modify the code so that it will process any faster than this? It seems like the bottleneck may be the design of the method and the data's structure. Else, how could scrollers achieve the speed that they get? Appreciate the assistance! theagentd: I do not have a .rar compressor; however, I uploaded it as a .zip to the following URL: http://www.mediafire.com/?md5faqehgkub4z2I would of course appreciate if you could take a look and see if there is anything that you think could be done to speed it up :] It doesn't seem to me like a side scroller should be a troublesome program to make, but I think I just have it designed improperly. Thank you guys very much! Looking forward to see if there is any possible solution in sight. Colton
|
|
|
|
|
ra4king
JGO Kernel      Posts: 3160 Medals: 196
I'm the King!
|
 |
«
Reply #4 on:
2011-10-11 00:25:30 » |
|
Have you tried using a debugger? Also try putting println()s that time how long method calls take.
|
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #5 on:
2011-10-11 00:49:35 » |
|
ra4king,
Btw, I forgot to say, yes, I had watched the Bucky videos. :p He actually derived much of what he used in that video from a book called Developing Games in Java, which I realized once I gave it a read; that, or the two are just uncannily similar. I like his manner and way of explaining things; he does a good job.
Anywho, I used the Profiler in Netbeans and found that the drawLevel method was taking up 21% of the program's CPU usage, whereas nearly every other method didn't even scratch the surface. When it has to redraw the whole level view, it usually takes up 26 ms, whereas otherwise it takes up about 0-1 to just shift the view.
Colton
|
|
|
|
|
ra4king
JGO Kernel      Posts: 3160 Medals: 196
I'm the King!
|
 |
«
Reply #6 on:
2011-10-11 02:08:16 » |
|
Well since you are drawing a 1280x768 BufferedImage to another Image, that sounds about right.
|
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #7 on:
2011-10-11 02:20:37 » |
|
ra4king,
Have you seen or played the game Terraria?
|
|
|
|
|
dishmoth
JGO Ninja    Posts: 517 Medals: 19
|
 |
«
Reply #8 on:
2011-10-11 05:50:06 » |
|
Some comments. Don't know if they'll help.
- +1 for what ra4king said about the correct use of BufferStrategy.
- Create 'compatible' images rather than instantiating BufferedImages directly. That is, use GraphicsConfiguration.createCompatibleImage(width,height,transparency). Search the forums for more details. I can't promise it will help, but in theory it will mean that the BufferedImage can use hardware acceleration.
- Don't call nanoSeconds() all over the place. Calculate the time that has passed since the last update in the game loop, and pass that 'delta time' to the update methods. If each update method calculates its own delta time, there's a danger of different game objects getting out of sync.
- As I recall, the Java documentation recommends creating then disposing of Graphics objects whenever you change a BufferedImage, rather than hanging on to a reference to the same one indefinitely. I don't know how much this affects things in practice.
- You can cast your Graphics references to Graphics2D if you like. This will give you more functions to play with.
- Your KeyListener methods should just record which keys are pressed or released. They shouldn't do any real work. Currently you appear to be using the methods to redraw the entire level. Calls to the update functions should happen in the game loop itself, not in the key listeners. The problem is that the key events are processed in a different thread to the game loop, and you're going to run into some nasty thread-safety glitches if you're not careful.
- Try chopping your code down so that it's just a game loop that redraws the level tiles each frame. That'll be easier to debug (for you and, more importantly, for us!).
Simon
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #9 on:
2011-10-11 05:55:48 » |
|
I tried to run the code and got constant 60 FPS. My laptop owned your gaming beast... >_>
|
There is no god.
|
|
|
Games published by our own members! Go get 'em!
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #10 on:
2011-10-11 06:15:54 » |
|
I tried to run the code and got constant 60 FPS. My laptop owned your gaming beast... >_>
I get 60 fps on my computer as well. It's not the FPS that's the issue, it's how fast the scrolling is on the level that's bugging me. But thank you for taking a look at the code, in any case, theagentd. Simon, Thank you for all those tips... I'm going to go over them tomorrow and refactor my code to reflect them. (Gotta hit the sack soon.) All of those make sense, and since this is my first really big game project, I want to get as many things right as I can, for now and for future games. Once I've made the changes, I'll post back to inform everyone of how those tips helped the code. I'm hoping there's some way I can get this to all work without having to resort to learning OpenGL. XD Colton
|
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #11 on:
2011-10-11 07:27:09 » |
|
... How can it be slow and have constant 60 FPS?! Your problem has absolutely nothing to do with rendering. It's your input handling that's the problem. Just move the input handling to the game loop and do things properly and your "slowness" will magically disappear. You should have described your problem better...
|
There is no god.
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #12 on:
2011-10-11 18:34:37 » |
|
theagentd,
Forgive me for not explaining it better. I will work on fixing that soon and post regarding the performance increase. Question, though: Must I place the KeyListener in main instead of in the LevelRenderer's run method, or is there an alternate means of accomplishing this? I appreciate your help.
Colton
|
|
|
|
|
ra4king
JGO Kernel      Posts: 3160 Medals: 196
I'm the King!
|
 |
«
Reply #13 on:
2011-10-11 19:40:45 » |
|
Yes I have heard of Terraria. But what does that have to do with this topic?
Where KeyListener is and the way you have things set up is fine for now. If you want to get rid of the pause that comes between the first key press and all further key presses when holding down a key, use your listeners to turn boolean variables that hold the state of certain keys into true if the key is pressed and false when released.
If your game runs at 60FPS, then there is nothing wrong with rendering. What exactly is being slow?
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #14 on:
2011-10-11 20:51:21 » |
|
Just keep a boolean for each key (boolean leftKey, rightKey, etc;) and in keyPressed() set the right boolean to true. In keyReleased() set it to false. Then in your gameloop (your run() method) do a check for each of the key booleans and update the player's movement according to which keys are pressed. You're slowness is as I've said not related to rendering or computer performance at all. It's how the logic is done. - Don't call nanoSeconds() all over the place. Calculate the time that has passed since the last update in the game loop, and pass that 'delta time' to the update methods. If each update method calculates its own delta time, there's a danger of different game objects getting out of sync. Definitely do this. When you've done that, move all the logic out of the key listener functions. If your game runs at 60FPS, then there is nothing wrong with rendering. What exactly is being slow?
The player movement.
|
There is no god.
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #15 on:
2011-10-11 22:29:26 » |
|
ra4king and theagentd, I was just asking because that kind of smooth scrolling is the kind I'd like to see with my game... and the same sort of tile size. But yes, I will update the nanoSeconds() functions so that they are passed in so everything is universal. I appreciate the advice. Also, I've changed the logic of the whole program so that the update methods switch booleans on and off, and as a result, I've noticed a lot of improvement and it looks much smoother. However, the character movement is still on the slow side, much more so that what I'm going for. I'll post the updated code below. LevelRenderer: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
| import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import javax.imageio.*; import javax.swing.JFrame; import javax.swing.*; import java.util.Random; import java.awt.Color;
public class LevelRenderer extends JFrame { private final int TILE_SIZE = 32; private final int SCREEN_WIDTH = 1280; private final int SCREEN_HEIGHT = 768; private BufferStrategy buffer; private Player myPlayer; private Level myLevel; private Screen s; private Graphics graphics; private boolean endGame; public LevelRenderer() { setPreferredSize(new Dimension(1280, 768)); setIgnoreRepaint( true ); setUndecorated( true );
setFocusable(true); requestFocus(); setResizable(false); addKeyListener( new KeyAdapter() { public void keyPressed(KeyEvent e) { processKey(e); } public void keyReleased(KeyEvent e) { processRelease(e); } }); myPlayer = new Player(); myLevel = new Level("obstaclemap"); endGame = false; } public static BufferedImage loadImage(String ref) { BufferedImage bimg = null; try { bimg = ImageIO.read(new File(ref)); } catch (Exception e) { e.printStackTrace(); } return bimg; } public void run(DisplayMode dm) { setBackground(Color.WHITE); s = new Screen(); s.setFullScreen(dm, this); this.createBufferStrategy( 2 ); buffer = this.getBufferStrategy(); int fps = 0; int frames = 0; long totalTime = 0; long curTime = System.currentTimeMillis(); long lastTime = curTime;
while (!endGame) { do { do { try { lastTime = curTime; curTime = System.currentTimeMillis(); totalTime += curTime - lastTime; if( totalTime > 1000 ) { totalTime -= 1000; fps = frames; frames = 0; } ++frames; graphics = buffer.getDrawGraphics(); myPlayer.animatePlayer(); myLevel.updateOffsets(); myLevel.drawLevel(graphics, 0, 0); myPlayer.drawPlayer(graphics, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE); graphics.setFont( new Font( "Courier New", Font.PLAIN, 12 ) ); graphics.setColor( Color.GREEN ); graphics.drawString( String.format( "FPS: %s", fps ), 20, 20 ); graphics.dispose(); } catch (Exception ex) { System.err.println("Game Update Error: " + ex); } try { Thread.sleep(2); } catch (Exception ex) { System.out.println("Can't sleep!"); } } while (buffer.contentsRestored()); buffer.show(); } while (buffer.contentsLost()); }
s.restoreScreen(); } public void processKey(KeyEvent e) { int keyCode = e.getKeyCode(); boolean moved = false; int xDisplace, yDisplace; if (keyCode == KeyEvent.VK_ESCAPE) { endGame = true; } switch (keyCode) { case KeyEvent.VK_UP: myPlayer.updatePlayer(0); myLevel.updateLevel(0); break; case KeyEvent.VK_LEFT: myPlayer.updatePlayer(1); myLevel.updateLevel(1); break; case KeyEvent.VK_RIGHT: myPlayer.updatePlayer(2); myLevel.updateLevel(2); break; case KeyEvent.VK_DOWN: myPlayer.updatePlayer(3); myLevel.updateLevel(3); break; case KeyEvent.VK_SPACE: myPlayer.updatePlayer(4); break; } } public void processRelease(KeyEvent e) { int keyCode = e.getKeyCode(); boolean moved = false; int xDisplace, yDisplace; switch (keyCode) { case KeyEvent.VK_UP: myPlayer.updatePlayer(0); myLevel.updateLevel(5); break; case KeyEvent.VK_LEFT: myPlayer.updatePlayer(5); myLevel.updateLevel(6); break; case KeyEvent.VK_RIGHT: myPlayer.updatePlayer(6); myLevel.updateLevel(7); break; case KeyEvent.VK_DOWN: myPlayer.updatePlayer(3); myLevel.updateLevel(8); break; case KeyEvent.VK_SPACE: myPlayer.updatePlayer(4); break; } } }
|
Level: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
|
import java.awt.image.*; import java.io.*; import javax.imageio.*; import java.awt.*;
public class Level { private final int TILE_SIZE = 32; private final int SCREEN_WIDTH = 1280; private final int SCREEN_HEIGHT = 768; private BufferedImage levelImage; private int width, height; private String levelName; private LevelCollisions myCollisions; private int levelTiles[][]; private BufferedImage tileSheet; private BufferedImage[] tiles; private BufferedImage cameraImage; private BufferedImage sideBufferL, sideBufferR, sideBufferT, sideBufferB; private Graphics cameraG; private int offsetX, offsetY; private int coordX, coordY; private boolean isLeft, isUp, isDown, isRight; private static final int SPACE_COLOR = 0xFF000000; private static final int WALL_COLOR = 0xFFFFFFFF; public Level(String level) { levelName = level; levelImage = loadImage(level + ".png"); myCollisions = new LevelCollisions(level + "Collision"); levelTiles = loadLevel(); cameraImage = new BufferedImage(1280, 768, BufferedImage.TYPE_INT_ARGB); cameraImage.createGraphics(); cameraG = cameraImage.getGraphics(); offsetX = offsetY = 0; isLeft = isRight = isUp = isDown = false; coordX = 600; coordY = 383; tileSheet = loadImage("obstacletiles.png"); tiles = splitImage(tileSheet, 2, 1); this.redrawCamera(); } public int[][] loadLevel() { height = levelImage.getHeight(); width = levelImage.getWidth(); int levelValues[][] = new int[width][height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { levelValues[x][y] = levelImage.getRGB(x, y); } } return levelValues; } public int getTile(int x, int y) { return levelTiles[x][y]; } public void drawLevel(Graphics gr, int x, int y) { gr.drawImage(cameraImage, x + offsetX, y + offsetY, null); } public void redrawCamera() { int x, y; int tileX, tileY; tileY = coordY; for (y = 0; y < SCREEN_HEIGHT; y += TILE_SIZE) { tileX = coordX; for (x = 0; x < SCREEN_WIDTH; x += TILE_SIZE) { switch (this.getTile(tileX, tileY)) { case SPACE_COLOR: cameraG.drawImage(tiles[0], x, y, null); break; case WALL_COLOR: cameraG.drawImage(tiles[1], x, y, null); break; } tileX++; } tileY++; } if (offsetX > 0) { } if (offsetX < 0) { } if (offsetY < 0) { } if (offsetY > 0) { } } public void updateOffsets() { if (isUp) { if (coordY > 30) { offsetY += 2; } if (offsetY >= TILE_SIZE) { offsetY = 0; coordY--; this.redrawCamera(); } } if (isLeft) { if (coordX > 30) { offsetX += 2; } if (offsetX >= TILE_SIZE) { offsetX = 0; coordX--; this.redrawCamera(); } } if (isRight) { if (coordX < width - 30) { offsetX -= 2; } if (offsetX <= -TILE_SIZE) { offsetX = 0; coordX++; this.redrawCamera(); } } if (isDown) { if (coordY < height - 30) { offsetY -= 2; } if (offsetY <= -TILE_SIZE) { offsetY = 0; coordY++; this.redrawCamera(); } } } public void updateLevel(int input) { switch (input) { case 0: isUp = true; break; case 1: isLeft = true; break; case 2: isRight = true; break; case 3: isDown = true; break; case 5: isUp = false; break; case 6: isLeft = false; break; case 7: isRight = false; break; case 8: isDown = false; break; case 4: break; } } public static BufferedImage loadImage(String ref) { BufferedImage bimg = null; try { bimg = ImageIO.read(new File(ref)); } catch (Exception e) { e.printStackTrace(); } return bimg; } public static BufferedImage[] splitImage(BufferedImage img, int cols, int rows) { int w = img.getWidth() / cols; int h = img.getHeight() / rows; int num = 0; BufferedImage imgs[] = new BufferedImage[w * h]; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { imgs[num] = new BufferedImage(w, h, img.getType()); Graphics2D g = imgs[num].createGraphics(); g.drawImage(img, 0, 0, w, h, w * x, h * y, w * x + w, h * y + h, null); g.dispose(); num++; } } return imgs; } public static BufferedImage makeColorTransparent(String ref, int color) { BufferedImage image = loadImage(ref); BufferedImage dimg = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = dimg.createGraphics(); g.setComposite(AlphaComposite.Src); g.drawImage(image, null, 0, 0); g.dispose(); for (int i = 0; i < dimg.getHeight(); i++) { for (int j = 0; j < dimg.getWidth(); j++) { if (dimg.getRGB(j, i) == color) { dimg.setRGB(j, i, 0x8F1C1C); } } } return dimg; } }
|
I hope I've got things now in the right place, or at least closer to how they should be. Thank you guys very much for your help! Not that it probably matters too much, but I gave both of you appreciation. ;] Colton
|
|
|
|
|
ra4king
JGO Kernel      Posts: 3160 Medals: 196
I'm the King!
|
 |
«
Reply #16 on:
2011-10-11 22:36:25 » |
|
Why are you sleeping for 2 milliseconds in your loop?
|
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #17 on:
2011-10-11 23:33:11 » |
|
ra4king,
I read in a few places that it's good to sleep in situations like these to let the other threads process more efficiently. Is this unnecessary?
Colton
|
|
|
|
|
ra4king
JGO Kernel      Posts: 3160 Medals: 196
I'm the King!
|
 |
«
Reply #18 on:
2011-10-11 23:52:56 » |
|
Your game loop should be following a simple flow: update -> render -> sleep. However the sleep should be the amount you want to sleep to achieve the target FPS minus the amount of time it took to update and render, like this example.
|
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #19 on:
2011-10-12 00:13:58 » |
|
ra4king, Thank you very much for all the advice! I'm learning a lot. I did what you told me and created a couple bits that calculate the sleep amount using the example you showed. I also made a render() and update() function that are now called within the game loop instead, which I think is much more readable, as well. Here's the updated code for LevelRenderer: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
| import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.io.*; import javax.imageio.*; import javax.swing.JFrame; import javax.swing.*; import java.util.Random; import java.awt.Color;
public class LevelRenderer extends JFrame { private final int TILE_SIZE = 32; private final int SCREEN_WIDTH = 1280; private final int SCREEN_HEIGHT = 768; private BufferStrategy buffer; private Player myPlayer; private Level myLevel; private Screen s; private Graphics graphics; private boolean endGame; private int fps = 0; private int frames = 0; private long totalTime = 0; private long curTime = System.currentTimeMillis(); private long lastTime = curTime; private long now = 0; private long rest = now; public LevelRenderer() { setPreferredSize(new Dimension(1280, 768)); setIgnoreRepaint( true ); setUndecorated( true );
setFocusable(true); requestFocus(); setResizable(false); addKeyListener( new KeyAdapter() { public void keyPressed(KeyEvent e) { processKey(e); } public void keyReleased(KeyEvent e) { processRelease(e); } }); myPlayer = new Player(); myLevel = new Level("obstaclemap"); endGame = false; } public static BufferedImage loadImage(String ref) { BufferedImage bimg = null; try { bimg = ImageIO.read(new File(ref)); } catch (Exception e) { e.printStackTrace(); } return bimg; } public void run(DisplayMode dm) { setBackground(Color.WHITE); s = new Screen(); s.setFullScreen(dm, this); this.createBufferStrategy( 2 ); buffer = this.getBufferStrategy();
while (!endGame) { do { do { try { update(); render(); } catch (Exception ex) { System.err.println("Game Update Error: " + ex); } try { Thread.sleep(rest); } catch (Exception ex) { System.out.println("Can't sleep!"); } } while (buffer.contentsRestored()); buffer.show(); } while (buffer.contentsLost()); }
s.restoreScreen(); } private void update() { lastTime = curTime; curTime = System.currentTimeMillis(); totalTime += curTime - lastTime; if( totalTime > 1000 ) { totalTime -= 1000; fps = frames; frames = 0; } ++frames; now = System.currentTimeMillis(); graphics = buffer.getDrawGraphics(); myPlayer.animatePlayer(); myLevel.updateOffsets(); } private void render() { myLevel.drawLevel(graphics, 0, 0); myPlayer.drawPlayer(graphics, (SCREEN_WIDTH / 2) - TILE_SIZE / 2, (SCREEN_HEIGHT / 2) - TILE_SIZE); graphics.setFont( new Font( "Courier New", Font.PLAIN, 12 ) ); graphics.setColor( Color.GREEN ); graphics.drawString( String.format( "FPS: %s", fps ), 20, 20 ); rest = 1000 / 60 - (System.currentTimeMillis() - now); if (rest < 0) { rest = 0; } graphics.dispose(); } public void processKey(KeyEvent e) { int keyCode = e.getKeyCode(); boolean moved = false; int xDisplace, yDisplace; if (keyCode == KeyEvent.VK_ESCAPE) { endGame = true; } switch (keyCode) { case KeyEvent.VK_UP: myPlayer.updatePlayer(0); myLevel.updateLevel(0); break; case KeyEvent.VK_LEFT: myPlayer.updatePlayer(1); myLevel.updateLevel(1); break; case KeyEvent.VK_RIGHT: myPlayer.updatePlayer(2); myLevel.updateLevel(2); break; case KeyEvent.VK_DOWN: myPlayer.updatePlayer(3); myLevel.updateLevel(3); break; case KeyEvent.VK_SPACE: myPlayer.updatePlayer(4); break; } } public void processRelease(KeyEvent e) { int keyCode = e.getKeyCode(); boolean moved = false; int xDisplace, yDisplace; switch (keyCode) { case KeyEvent.VK_UP: myPlayer.updatePlayer(0); myLevel.updateLevel(5); break; case KeyEvent.VK_LEFT: myPlayer.updatePlayer(5); myLevel.updateLevel(6); break; case KeyEvent.VK_RIGHT: myPlayer.updatePlayer(6); myLevel.updateLevel(7); break; case KeyEvent.VK_DOWN: myPlayer.updatePlayer(3); myLevel.updateLevel(8); break; case KeyEvent.VK_SPACE: myPlayer.updatePlayer(4); break; } } }
|
Any thoughts left on what I could do make the level scroll faster? Appreciate all the assistance. Colton
|
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #20 on:
2011-10-12 05:48:14 » |
|
If the game is running at 60 FPS you are displaying as many frames as the screen can show. Increasing the FPS of the game further is out of the question, as it will increase the speed of everything in the game PLUS making the game require better hardware to run. What you want to do to achieve faster movement is just to increase the movement speed of the player, right? I haven't been able to figure out how you actually move the player (e.g. update it's coordinates based on input, TL;DR it all you know...) so I can't tell you exactly how to do it in your code, but you should do movement like this: 1 2 3 4
| if(upKeyDown){ player.y -= delta * moveSpeed; }
|
- player.y is the current position of the player (you'll probably not write it exactly like this (public access), but whatever) - moveSpeed is a variable that decides the speed of movement (DR. OBVIOUS TO THE RESCUE). - delta is the time since the last update, which should be calculated universally for all objects in the game in the game loop. I think you should add delta calculation to your game loop: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| long time = System.currentTimeMillis(); while (!endGame) { do { do { try { long currentTime = System.currentTimeMillis(); int delta = (int)(currentTime - time); time = currentTime; update(delta); render(); } catch (Exception ex) { System.err.println("Game Update Error: " + ex); } try { Thread.sleep(rest); } catch (Exception ex) { System.out.println("Can't sleep!"); } } while (buffer.contentsRestored()); buffer.show(); } while (buffer.contentsLost()); } |
All your update methods should take an int delta parameter, and use it to determine how far to move things each update. Doing it like this also makes sure that the game runs at the same speed, regardless of how fast the computer running the game is. Even though the game only runs at 30FPS for example, the objects would make double as long "jumps" each update so they will move the same distance in the same time. I hope things have cleared up a bit. xD
|
There is no god.
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #21 on:
2011-10-13 04:48:55 » |
|
theagentd,
Thank you very much... that's been the problem all along, and I was unable to pinpoint it. It now makes sense... the player's been moving at the slowest possible speed this whole time. The delta method also makes perfect sense now with that illustrated example. I'll get to work updating everything and trying to tie it all together. I appreciate your help and your examples; you've been very helpful.
Kind regards, Colton
|
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #22 on:
2011-10-13 05:03:46 » |
|
No problem. Those who ask questions are the ones that learn the most. 
|
There is no god.
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #23 on:
2011-10-13 05:25:34 » |
|
Lol yes, indeed. I know I have a crap ton to learn. Before this, I was just doing highly simple, static-screen based games like Pong. But I implemented a rough version of what you just explained to me, not everything yet but the most important parts, and it works wonderfully, just like I wanted it to. Thanks so much! I'm sure you'll have plenty of questions to answer from me on the forums in the future (;]) and I know I can come here for help from now on without a problem.
Colton
|
|
|
|
|
theagentd
JGO Wizard     Posts: 1392 Medals: 88
|
 |
«
Reply #24 on:
2011-10-13 05:27:51 » |
|
Yeah, well, I'm not going anywhere. xp
|
There is no god.
|
|
|
coltonoscopy
JGO n00b  Posts: 29 Medals: 1
|
 |
«
Reply #25 on:
2011-10-13 05:29:27 » |
|
Thank goodness.... you're the man! 
|
|
|
|
|
|