1 | package io.github.some_example_name; |
2 | |
3 | import com.badlogic.gdx.ApplicationListener; |
4 | import com.badlogic.gdx.Gdx; |
5 | import com.badlogic.gdx.InputAdapter; |
6 | import com.badlogic.gdx.Input.Keys; |
7 | import com.badlogic.gdx.graphics.Color; |
8 | import com.badlogic.gdx.graphics.GL20; |
9 | import com.badlogic.gdx.graphics.OrthographicCamera; |
10 | import com.badlogic.gdx.graphics.Texture; |
11 | import com.badlogic.gdx.graphics.glutils.ShapeRenderer; |
12 | import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; |
13 | import com.badlogic.gdx.maps.tiled.TiledMap; |
14 | import com.badlogic.gdx.maps.tiled.TiledMapTileLayer; |
15 | import com.badlogic.gdx.maps.tiled.TiledMapTileLayer.Cell; |
16 | import com.badlogic.gdx.maps.tiled.TmxMapLoader; |
17 | import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer; |
18 | import com.badlogic.gdx.math.MathUtils; |
19 | import com.badlogic.gdx.math.Rectangle; |
20 | import com.badlogic.gdx.math.Vector2; |
21 | import com.badlogic.gdx.utils.Array; |
22 | import com.badlogic.gdx.utils.Pool; |
23 | import com.badlogic.gdx.utils.ScreenUtils; |
24 | import com.badlogic.gdx.*; |
25 | import com.badlogic.gdx.graphics.*; |
26 | import com.badlogic.gdx.graphics.g2d.*; |
27 | import com.badlogic.gdx.math.*; |
28 | import com.badlogic.gdx.scenes.scene2d.ui.Label; |
29 | import com.badlogic.gdx.utils.*; |
30 | import com.badlogic.gdx.maps.*; |
31 | import com.badlogic.gdx.maps.tiled.*; |
32 | import com.badlogic.gdx.graphics.glutils.*; |
33 | |
34 | |
35 | /** Super Mario Brothers-like very basic platformer, using a tile map built using <a href="https://www.mapeditor.org/">Tiled</a> and a |
36 | * tileset and sprites by <a href="http://www.vickiwenderlich.com/">Vicky Wenderlich</a></p> |
37 | * |
38 | * Shows simple platformer collision detection as well as on-the-fly map modifications through destructible blocks! |
39 | * @author mzechner */ |
40 | public class Main extends InputAdapter implements ApplicationListener { |
41 | private boolean gameOver = false; // Game Over flag |
42 | private String gameOverMessage = "Game Over press R to Restart"; |
43 | /** The player character, has state and state time, */ |
44 | static class Koala { |
45 | static float WIDTH; |
46 | static float HEIGHT; |
47 | static float MAX_VELOCITY = 10f; |
48 | static float JUMP_VELOCITY = 40f; |
49 | static float DAMPING = 0.87f; |
50 | |
51 | enum State { |
52 | Standing, Walking, Jumping |
53 | } |
54 | |
55 | final Vector2 position = new Vector2(); |
56 | final Vector2 velocity = new Vector2(); |
57 | State state = State.Walking; |
58 | float stateTime = 0; |
59 | boolean facesRight = true; |
60 | boolean grounded = false; |
61 | } |
62 | |
63 | private TiledMap map; |
64 | private OrthogonalTiledMapRenderer renderer; |
65 | private OrthographicCamera camera; |
66 | private Texture koalaTexture; |
67 | private Animation<TextureRegion> stand; |
68 | private Animation<TextureRegion> walk; |
69 | private Animation<TextureRegion> jump; |
70 | private Koala koala; |
71 | private Pool<Rectangle> rectPool = new Pool<Rectangle>() { |
72 | @Override |
73 | protected Rectangle newObject () { |
74 | return new Rectangle(); |
75 | } |
76 | }; |
77 | private Array<Rectangle> tiles = new Array<Rectangle>(); |
78 | |
79 | private static final float GRAVITY = -2.5f; |
80 | |
81 | private boolean debug = false; |
82 | private ShapeRenderer debugRenderer; |
83 | |
84 | @Override |
85 | public void create () { |
86 | |
87 | // load the koala frames, split them, and assign them to Animations |
88 | koalaTexture = new Texture("koalio.png"); |
89 | TextureRegion[] regions = TextureRegion.split(koalaTexture, 18, 26)[0]; |
90 | stand = new Animation<TextureRegion>(0, regions[0]); |
91 | jump = new Animation<TextureRegion>(0, regions[1]); |
92 | walk = new Animation<TextureRegion>(0.15f, regions[2], regions[3], regions[4]); |
93 | walk.setPlayMode(Animation.PlayMode.LOOP_PINGPONG); |
94 | |
95 | // figure out the width and height of the koala for collision |
96 | // detection and rendering by converting a koala frames pixel |
97 | // size into world units (1 unit == 16 pixels) |
98 | Koala.WIDTH = 1 / 16f * regions[0].getRegionWidth(); |
99 | Koala.HEIGHT = 1 / 16f * regions[0].getRegionHeight(); |
100 | |
101 | // load the map, set the unit scale to 1/16 (1 unit == 16 pixels) |
102 | map = new TmxMapLoader().load("level1.tmx"); |
103 | renderer = new OrthogonalTiledMapRenderer(map, 1 / 16f); |
104 | |
105 | // create an orthographic camera, shows us 30x20 units of the world |
106 | camera = new OrthographicCamera(); |
107 | camera.setToOrtho(false, 30, 20); |
108 | camera.update(); |
109 | |
110 | // create the Koala we want to move around the world |
111 | koala = new Koala(); |
112 | koala.position.set(20, 20); |
113 | |
114 | debugRenderer = new ShapeRenderer(); |
115 | } |
116 | |
117 | @Override |
118 | public void render () { |
119 | // Clear the screen |
120 | ScreenUtils.clear(0.7f, 0.7f, 1.0f, 1); |
121 | |
122 | if (gameOver) { |
123 | // Render Game Over screen |
124 | renderGameOver(); |
125 | return; // Skip further updates |
126 | } |
127 | |
128 | // Get the delta time |
129 | float deltaTime = Gdx.graphics.getDeltaTime(); |
130 | |
131 | // Update the koala (process input, collision detection, position update) |
132 | updateKoala(deltaTime); |
133 | |
134 | // Let the camera follow the koala, x-axis only |
135 | camera.position.x = koala.position.x; |
136 | camera.update(); |
137 | |
138 | // Set the TiledMapRenderer view based on what the |
139 | // camera sees, and render the map |
140 | renderer.setView(camera); |
141 | renderer.render(); |
142 | |
143 | // Render the koala |
144 | renderKoala(deltaTime); |
145 | |
146 | // Render debug rectangles |
147 | if (debug) renderDebug(); |
148 | |
149 | } |
150 | |
151 | private void updateKoala (float deltaTime) { |
152 | |
153 | if (deltaTime == 0) return; |
154 | |
155 | if (gameOver) return; // Stop updates if game over |
156 | |
157 | // Check for game over condition |
158 | if (koala.position.y < -10) { // Example: Falling below -10 triggers game over |
159 | gameOver = true; |
160 | return; |
161 | } |
162 | |
163 | |
164 | if (deltaTime == 0) return; |
165 | |
166 | if (deltaTime > 0.1f) |
167 | deltaTime = 0.1f; |
168 | |
169 | koala.stateTime += deltaTime; |
170 | |
171 | // check input and apply to velocity & state |
172 | if ((Gdx.input.isKeyPressed(Keys.SPACE) || isTouched(0.5f, 1)) && koala.grounded) { |
173 | koala.velocity.y += Koala.JUMP_VELOCITY; |
174 | koala.state = Koala.State.Jumping; |
175 | koala.grounded = false; |
176 | } |
177 | |
178 | if (Gdx.input.isKeyPressed(Keys.LEFT) || Gdx.input.isKeyPressed(Keys.A) || isTouched(0, 0.25f)) { |
179 | koala.velocity.x = -Koala.MAX_VELOCITY; |
180 | if (koala.grounded) koala.state = Koala.State.Walking; |
181 | koala.facesRight = false; |
182 | } |
183 | |
184 | if (Gdx.input.isKeyPressed(Keys.RIGHT) || Gdx.input.isKeyPressed(Keys.D) || isTouched(0.25f, 0.5f)) { |
185 | koala.velocity.x = Koala.MAX_VELOCITY; |
186 | if (koala.grounded) koala.state = Koala.State.Walking; |
187 | koala.facesRight = true; |
188 | } |
189 | |
190 | if (Gdx.input.isKeyJustPressed(Keys.B)) |
191 | debug = !debug; |
192 | |
193 | // apply gravity if we are falling |
194 | koala.velocity.add(0, GRAVITY); |
195 | |
196 | // clamp the velocity to the maximum, x-axis only |
197 | koala.velocity.x = MathUtils.clamp(koala.velocity.x, |
198 | -Koala.MAX_VELOCITY, Koala.MAX_VELOCITY); |
199 | |
200 | // If the velocity is < 1, set it to 0 and set state to Standing |
201 | if (Math.abs(koala.velocity.x) < 1) { |
202 | koala.velocity.x = 0; |
203 | if (koala.grounded) koala.state = Koala.State.Standing; |
204 | } |
205 | |
206 | // multiply by delta time so we know how far we go |
207 | // in this frame |
208 | koala.velocity.scl(deltaTime); |
209 | |
210 | // perform collision detection & response, on each axis, separately |
211 | // if the koala is moving right, check the tiles to the right of it's |
212 | // right bounding box edge, otherwise check the ones to the left |
213 | Rectangle koalaRect = rectPool.obtain(); |
214 | koalaRect.set(koala.position.x, koala.position.y, Koala.WIDTH, Koala.HEIGHT); |
215 | int startX, startY, endX, endY; |
216 | if (koala.velocity.x > 0) { |
217 | startX = endX = (int)(koala.position.x + Koala.WIDTH + koala.velocity.x); |
218 | } else { |
219 | startX = endX = (int)(koala.position.x + koala.velocity.x); |
220 | } |
221 | startY = (int)(koala.position.y); |
222 | endY = (int)(koala.position.y + Koala.HEIGHT); |
223 | getTiles(startX, startY, endX, endY, tiles); |
224 | koalaRect.x += koala.velocity.x; |
225 | for (Rectangle tile : tiles) { |
226 | if (koalaRect.overlaps(tile)) { |
227 | koala.velocity.x = 0; |
228 | break; |
229 | } |
230 | } |
231 | koalaRect.x = koala.position.x; |
232 | |
233 | // if the koala is moving upwards, check the tiles to the top of its |
234 | // top bounding box edge, otherwise check the ones to the bottom |
235 | if (koala.velocity.y > 0) { |
236 | startY = endY = (int)(koala.position.y + Koala.HEIGHT + koala.velocity.y); |
237 | } else { |
238 | startY = endY = (int)(koala.position.y + koala.velocity.y); |
239 | } |
240 | startX = (int)(koala.position.x); |
241 | endX = (int)(koala.position.x + Koala.WIDTH); |
242 | getTiles(startX, startY, endX, endY, tiles); |
243 | koalaRect.y += koala.velocity.y; |
244 | for (Rectangle tile : tiles) { |
245 | if (koalaRect.overlaps(tile)) { |
246 | // we actually reset the koala y-position here |
247 | // so it is just below/above the tile we collided with |
248 | // this removes bouncing :) |
249 | if (koala.velocity.y > 0) { |
250 | koala.position.y = tile.y - Koala.HEIGHT; |
251 | // we hit a block jumping upwards, let's destroy it! |
252 | TiledMapTileLayer layer = (TiledMapTileLayer)map.getLayers().get("walls"); |
253 | layer.setCell((int)tile.x, (int)tile.y, null); |
254 | } else { |
255 | koala.position.y = tile.y + tile.height; |
256 | // if we hit the ground, mark us as grounded so we can jump |
257 | koala.grounded = true; |
258 | } |
259 | koala.velocity.y = 0; |
260 | break; |
261 | } |
262 | } |
263 | rectPool.free(koalaRect); |
264 | |
265 | // unscale the velocity by the inverse delta time and set |
266 | // the latest position |
267 | koala.position.add(koala.velocity); |
268 | koala.velocity.scl(1 / deltaTime); |
269 | |
270 | // Apply damping to the velocity on the x-axis so we don't |
271 | // walk infinitely once a key was pressed |
272 | koala.velocity.x *= Koala.DAMPING; |
273 | } |
274 | |
275 | // DIsplay |
276 | |
277 | private void renderGameOver() { |
278 | // Switch to a UI camera for rendering the "Game Over" text |
279 | float screenWidth = Gdx.graphics.getWidth(); |
280 | float screenHeight = Gdx.graphics.getHeight(); |
281 | |
282 | // Create a new orthographic camera that matches the screen size |
283 | OrthographicCamera uiCamera = new OrthographicCamera(); |
284 | uiCamera.setToOrtho(false, screenWidth, screenHeight); // Set the camera to match the screen size |
285 | uiCamera.update(); // Update the camera to apply the changes |
286 | |
287 | // Now, set the batch to use the UI camera's projection matrix |
288 | Batch batch = renderer.getBatch(); |
289 | batch.setProjectionMatrix(uiCamera.combined); |
290 | batch.begin(); |
291 | |
292 | // Create a new BitmapFont |
293 | BitmapFont font = new BitmapFont(); |
294 | |
295 | // Set the font color (you can keep it red or change it) |
296 | font.setColor(Color.RED); |
297 | |
298 | // Set the font scale to make it smaller |
299 | font.getData().setScale(2f); // Adjust this scale value to make it smaller (0.5f makes it half the normal size) |
300 | |
301 | // Get the width and height of the "GAME OVER" text |
302 | GlyphLayout layout = new GlyphLayout(font, "GAME OVER"); |
303 | float textWidth = layout.width; |
304 | float textHeight = layout.height; |
305 | |
306 | // Calculate the center position for the text |
307 | float x = (screenWidth - textWidth) / 2; // Horizontal centering |
308 | float y = (screenHeight + textHeight) / 2; // Vertical centering (adjusted to be more centered) |
309 | |
310 | // Log the position to check if the coordinates are correct |
311 | Gdx.app.log("GameOver", "Drawing 'GAME OVER' at x: " + x + ", y: " + y); |
312 | |
313 | |
314 | // Draw the "Game Over" text at the calculated position |
315 | font.draw(batch, "Game Over", x, y); // Render at the centers |
316 | |
317 | font.draw(batch, "Press C to Restart", x-40, y-35); // Render at the center |
318 | |
319 | batch.end(); |
320 | |
321 | if (Gdx.input.isKeyPressed(Keys.C)) { |
322 | restartGame(); |
323 | } |
324 | |
325 | |
326 | } |
327 | |
328 | |
329 | private void restartGame() { |
330 | gameOver = false; |
331 | |
332 | // Reset Koala position and state |
333 | koala.position.set(20, 20); |
334 | koala.velocity.set(0, 0); |
335 | koala.state = Koala.State.Standing; |
336 | |
337 | // Reload map or reset game state as necessary |
338 | map = new TmxMapLoader().load("level1.tmx"); |
339 | renderer.setMap(map); |
340 | } |
341 | |
342 | private boolean isTouched (float startX, float endX) { |
343 | // Check for touch inputs between startX and endX |
344 | // startX/endX are given between 0 (left edge of the screen) and 1 (right edge of the screen) |
345 | for (int i = 0; i < 2; i++) { |
346 | float x = Gdx.input.getX(i) / (float)Gdx.graphics.getBackBufferWidth(); |
347 | if (Gdx.input.isTouched(i) && (x >= startX && x <= endX)) { |
348 | return true; |
349 | } |
350 | } |
351 | return false; |
352 | } |
353 | |
354 | private void getTiles (int startX, int startY, int endX, int endY, Array<Rectangle> tiles) { |
355 | TiledMapTileLayer layer = (TiledMapTileLayer)map.getLayers().get("walls"); |
356 | rectPool.freeAll(tiles); |
357 | tiles.clear(); |
358 | for (int y = startY; y <= endY; y++) { |
359 | for (int x = startX; x <= endX; x++) { |
360 | Cell cell = layer.getCell(x, y); |
361 | if (cell != null) { |
362 | Rectangle rect = rectPool.obtain(); |
363 | rect.set(x, y, 1, 1); |
364 | tiles.add(rect); |
365 | } |
366 | } |
367 | } |
368 | } |
369 | |
370 | private void renderKoala (float deltaTime) { |
371 | // based on the koala state, get the animation frame |
372 | TextureRegion frame = null; |
373 | switch (koala.state) { |
374 | case Standing: |
375 | frame = stand.getKeyFrame(koala.stateTime); |
376 | break; |
377 | case Walking: |
378 | frame = walk.getKeyFrame(koala.stateTime); |
379 | break; |
380 | case Jumping: |
381 | frame = jump.getKeyFrame(koala.stateTime); |
382 | break; |
383 | } |
384 | |
385 | // draw the koala, depending on the current velocity |
386 | // on the x-axis, draw the koala facing either right |
387 | // or left |
388 | Batch batch = renderer.getBatch(); |
389 | batch.begin(); |
390 | if (koala.facesRight) { |
391 | batch.draw(frame, koala.position.x, koala.position.y, Koala.WIDTH, Koala.HEIGHT); |
392 | } else { |
393 | batch.draw(frame, koala.position.x + Koala.WIDTH, koala.position.y, -Koala.WIDTH, Koala.HEIGHT); |
394 | } |
395 | batch.end(); |
396 | } |
397 | |
398 | private void renderDebug () { |
399 | debugRenderer.setProjectionMatrix(camera.combined); |
400 | debugRenderer.begin(ShapeType.Line); |
401 | |
402 | debugRenderer.setColor(Color.RED); |
403 | debugRenderer.rect(koala.position.x, koala.position.y, Koala.WIDTH, Koala.HEIGHT); |
404 | |
405 | debugRenderer.setColor(Color.YELLOW); |
406 | TiledMapTileLayer layer = (TiledMapTileLayer)map.getLayers().get("walls"); |
407 | for (int y = 0; y <= layer.getHeight(); y++) { |
408 | for (int x = 0; x <= layer.getWidth(); x++) { |
409 | Cell cell = layer.getCell(x, y); |
410 | if (cell != null) { |
411 | if (camera.frustum.boundsInFrustum(x + 0.5f, y + 0.5f, 0, 1, 1, 0)) |
412 | debugRenderer.rect(x, y, 1, 1); |
413 | } |
414 | } |
415 | } |
416 | debugRenderer.end(); |
417 | } |
418 | |
419 | @Override |
420 | public void dispose () { |
421 | } |
422 | |
423 | @Override |
424 | public void resume () { |
425 | } |
426 | |
427 | @Override |
428 | public void resize(int width, int height) { |
429 | } |
430 | |
431 | @Override |
432 | public void pause() { |
433 | } |
434 | |
435 | |
436 | |
437 | |
438 | } |