Skip to content

Commit 7314caa

Browse files
refactor; feat: restart functionality
1 parent be52b62 commit 7314caa

File tree

6 files changed

+135
-46
lines changed

6 files changed

+135
-46
lines changed

games/running-ball-babylonjs/index.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
77
<link href="https://fonts.googleapis.com/css2?family=Black+Ops+One&display=swap" rel="stylesheet" />
88
<link rel="stylesheet" href="/src/style.css" />
9-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
9+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
1010
<title>Running Ball (Babylon.js + TypeScript)</title>
1111
<link rel="shortcut icon" href="/assets/images/football.png" type="image/x-icon" />
1212
</head>
@@ -23,7 +23,7 @@
2323
</div>
2424

2525
<div class="user-ui user-ui__bottom">
26-
<button id="restart-btn" class="ui-btn">
26+
<button id="restart-btn" class="restart-btn ui-btn">
2727
<img src="/assets/images/restart.png" width="30px" />
2828
</button>
2929
</div>
@@ -33,9 +33,11 @@
3333
<span class="game-over-info">GAME OVER</span>
3434
<span class="game-over-info" id="game-over-score-current">CURRENT SCORE</span>
3535
<span class="game-over-info" id="game-over-score-best">BEST SCORE</span>
36-
</div>
3736

38-
<span class="tap-to-restart">Tap to restart</span>
37+
<button id="restart-btn" class="restart-btn ui-btn">
38+
<img src="/assets/images/restart.png" width="30px" />
39+
</button>
40+
</div>
3941
</div>
4042
</main>
4143
<script type="module" src="/src/main.ts"></script>

games/running-ball-babylonjs/src/Game.ts

Lines changed: 85 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import {
2020
CubeTexture
2121
} from '@babylonjs/core';
2222
import '@babylonjs/loaders/glTF';
23-
import { WaterMaterial } from '@babylonjs/materials'
23+
import { WaterMaterial } from '@babylonjs/materials';
2424
//@ts-ignore
2525
import * as CANNON from 'cannon';
2626
import { GameStatus, type GameHTMLElementsRefs, type Size3D } from './types';
2727
import { notRepeatedRandomFreeSpacePositionGenerator } from './utils';
2828
import { GameStorage, StoredDataType } from './GameStorage';
29+
import { GameUiElementsManager } from './UiManager';
2930

3031
export class Game {
3132
private static readonly SinglePlatformSize: Size3D = Object.freeze({
@@ -36,7 +37,7 @@ export class Game {
3637
private static readonly SingleWallSize: Size3D = Object.freeze({
3738
height: Game.SinglePlatformSize.width / 3,
3839
depth: Game.SinglePlatformSize.width / 8,
39-
width: (Game.SinglePlatformSize.width / 3) - 0.075
40+
width: Game.SinglePlatformSize.width / 3 - 0.075
4041
});
4142
private static readonly ZeroVector = new Vector3(0, 0, 0);
4243
private static readonly StartSpeedOfMovingStraight = 6;
@@ -57,18 +58,21 @@ export class Game {
5758
private readonly ground: Mesh;
5859
private readonly camera: Camera;
5960
private readonly light: PointLight;
60-
private readonly platforms: Mesh[];
61-
private readonly walls: Mesh[];
62-
private readonly ball: Mesh;
63-
private readonly coins: Mesh[] = [];
61+
private platforms: Mesh[] = [];
62+
private walls: Mesh[] = [];
63+
private ball: Mesh = {} as Mesh;
64+
private coins: Mesh[] = [];
6465
private readonly shadowGenerator: ShadowGenerator;
6566
private readonly storage = new GameStorage();
67+
private readonly uiElementsManager: GameUiElementsManager;
6668
private coinScore: number = 0;
6769
private bestScore: number = 0;
6870
private gameStatus = GameStatus.Playing;
6971

7072
public constructor(private readonly elementsRefs: GameHTMLElementsRefs) {
71-
this.engine = new Engine(this.elementsRefs.canvas);
73+
this.uiElementsManager = new GameUiElementsManager(elementsRefs);
74+
75+
this.engine = new Engine(elementsRefs.canvas);
7276
this.scene = this.configureScene();
7377
this.light = this.configureLight();
7478
this.camera = this.configureCamera();
@@ -80,22 +84,19 @@ export class Game {
8084

8185
[this.wallMaterial, this.wallTouchedMaterial] = this.createWallMaterial();
8286
this.platformMaterial = this.createPlatformMaterial();
83-
84-
this.platforms = this.configurePlatform();
85-
void this.platforms;
86-
this.ball = this.configureBall();
87-
this.walls = this.createAllWalls();
8887
}
8988

9089
public init(): void {
91-
this.shrinkCanvas();
92-
window.addEventListener('resize', this.shrinkCanvas.bind(this));
90+
this.onWindowResize();
91+
window.addEventListener('resize', this.onWindowResize.bind(this));
9392

9493
this.initControls();
9594

95+
this.createGameObjects();
96+
9697
this.coinScore = this.storage.loadScore(StoredDataType.CurrentScore);
9798
this.bestScore = this.storage.loadScore(StoredDataType.BestScore);
98-
this.updateScoreText();
99+
this.uiElementsManager.updateScore(this.coinScore);
99100

100101
this.gameStatus = GameStatus.Playing;
101102

@@ -110,6 +111,12 @@ export class Game {
110111
});
111112
}
112113

114+
private createGameObjects(): void {
115+
this.platforms = this.configurePlatform();
116+
this.ball = this.configureBall();
117+
this.walls = this.createAllWalls();
118+
}
119+
113120
private createCoin(position: Vector3) {
114121
// I see, that deprecated, but Babylon is so strange, that the fastest way to load this
115122
// is just to use deprecated SceneLoader sync import
@@ -144,16 +151,25 @@ export class Game {
144151
}
145152

146153
private initControls() {
154+
for (const element of this.elementsRefs.restartButtons) {
155+
(element as HTMLButtonElement).onclick = this.restart.bind(this);
156+
}
157+
147158
window.addEventListener('keydown', (event) => {
148-
if (this.gameStatus !== GameStatus.Playing) return;
159+
if (this.gameStatus === GameStatus.GameOver && event.key === 'Enter') {
160+
this.restart();
161+
return;
162+
}
149163

150-
switch (true) {
151-
case event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a':
152-
this.pushBall(Game.MoveVectorLeft);
153-
break;
154-
case event.key === 'ArrowRight' || event.key.toLocaleLowerCase() === 'd':
155-
this.pushBall(Game.MoveVectorRight);
156-
break;
164+
if (this.ball.position.y < 2) {
165+
switch (true) {
166+
case event.key === 'ArrowLeft' || event.key.toLowerCase() === 'a':
167+
this.pushBall(Game.MoveVectorLeft);
168+
break;
169+
case event.key === 'ArrowRight' || event.key.toLocaleLowerCase() === 'd':
170+
this.pushBall(Game.MoveVectorRight);
171+
break;
172+
}
157173
}
158174
});
159175

@@ -191,7 +207,6 @@ export class Game {
191207
light.intensity = 0.4;
192208
return light;
193209
}
194-
195210

196211
private configureCamera(): Camera {
197212
const camera = new FreeCamera('camera', new Vector3(-2, 5, -10), this.scene);
@@ -267,22 +282,25 @@ export class Game {
267282
private configureSky() {
268283
const skyBox = MeshBuilder.CreateBox('skyBox', { size: 1000 }, this.scene);
269284
const skyBoxMaterial = new StandardMaterial('skyBox', this.scene);
270-
skyBoxMaterial.reflectionTexture = new CubeTexture('/assets/environments/TropicalSunnyDay/TropicalSunnyDay', this.scene);
285+
skyBoxMaterial.reflectionTexture = new CubeTexture(
286+
'/assets/environments/TropicalSunnyDay/TropicalSunnyDay',
287+
this.scene
288+
);
271289
skyBoxMaterial.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE;
272290
skyBoxMaterial.backFaceCulling = false;
273291
skyBox.material = skyBoxMaterial;
274292
return skyBox;
275293
}
276294

277295
private configureGround(): [Mesh, Mesh] {
278-
const water = MeshBuilder.CreateGround('water', { width: 512, height: 512}, this.scene);
296+
const water = MeshBuilder.CreateGround('water', { width: 512, height: 512 }, this.scene);
279297
water.position = new Vector3(0, -5, 0);
280298
const waterMaterial = new WaterMaterial('water', this.scene);
281299
waterMaterial.bumpTexture = new Texture('/assets/environments/waterbump.png', this.scene);
282300
waterMaterial.addToRenderList(this.sky);
283301
water.material = waterMaterial;
284302

285-
const ground = MeshBuilder.CreateGround('ground', { width: 512, height: 512}, this.scene);
303+
const ground = MeshBuilder.CreateGround('ground', { width: 512, height: 512 }, this.scene);
286304
ground.position = new Vector3(0, -10, 0);
287305
const groundMaterial = new StandardMaterial('ground', this.scene);
288306
groundMaterial.emissiveTexture = new Texture('/assets/environments/ground.jpg', this.scene);
@@ -291,8 +309,7 @@ export class Game {
291309
waterMaterial.addToRenderList(this.sky);
292310
waterMaterial.addToRenderList(ground);
293311

294-
295-
ground.physicsImpostor = new PhysicsImpostor(ground, PhysicsImpostor.BoxImpostor, { mass: 0 }, this.scene);
312+
ground.physicsImpostor = new PhysicsImpostor(ground, PhysicsImpostor.BoxImpostor, { mass: 0 }, this.scene);
296313

297314
return [water, ground];
298315
}
@@ -354,7 +371,7 @@ export class Game {
354371

355372
private checkIfGameOver(): void {
356373
if (this.ball.getAbsolutePosition().y <= 0) {
357-
this.gameOver();
374+
this.gameOver();
358375
}
359376

360377
const check = (spherePos: Vector3, box: BoundingBox): boolean => {
@@ -404,21 +421,16 @@ export class Game {
404421
this.light.position.z = this.ball.getAbsolutePosition().z + 10;
405422
}
406423

407-
private shrinkCanvas() {
408-
this.elementsRefs.canvas.width = this.elementsRefs.canvas.clientWidth;
409-
this.elementsRefs.canvas.height = this.elementsRefs.canvas.clientHeight;
424+
private onWindowResize() {
425+
this.uiElementsManager.shrinkCanvas();
410426
this.engine.resize();
411427
}
412428

413-
private updateScoreText() {
414-
this.elementsRefs.scoreText.innerText = String(this.coinScore);
415-
}
416-
417429
private checkCoinEarned(): void {
418430
for (let i = 0; i < this.coins.length; ++i) {
419431
if (this.ball.intersectsMesh(this.coins[i], true)) {
420432
++this.coinScore;
421-
this.updateScoreText();
433+
this.uiElementsManager.updateScore(this.coinScore);
422434
this.scene.removeMesh(this.coins[i]);
423435
this.coins[i].dispose();
424436
this.coins.splice(i, 1);
@@ -430,7 +442,7 @@ export class Game {
430442

431443
private gameOver(): void {
432444
this.gameStatus = GameStatus.GameOver;
433-
this.elementsRefs.gameOver.screen.style.display = 'flex';
445+
434446
const bestScore = this.storage.loadScore(StoredDataType.BestScore);
435447

436448
if (this.coinScore > bestScore) {
@@ -440,7 +452,39 @@ export class Game {
440452

441453
this.storage.saveScore(StoredDataType.CurrentScore, 0);
442454

443-
this.elementsRefs.gameOver.currentScore.innerText = `CURRENT SCORE: ${this.coinScore}`;
444-
this.elementsRefs.gameOver.bestScore.innerText = `BEST SCORE: ${this.bestScore}`;
455+
this.uiElementsManager.hideUiElements();
456+
this.uiElementsManager.showGameOverScreen(this.coinScore, this.bestScore);
457+
}
458+
459+
private resetGameObjects() {
460+
this.platforms.forEach((platform) => {
461+
this.scene.removeMesh(platform);
462+
platform.dispose();
463+
});
464+
465+
this.coins.forEach((coin) => {
466+
this.scene.removeMesh(coin);
467+
coin.dispose();
468+
});
469+
470+
this.walls.forEach((wall) => {
471+
this.scene.removeMesh(wall);
472+
wall.dispose();
473+
});
474+
475+
this.ball.dispose();
476+
this.scene.removeMesh(this.ball);
477+
478+
this.coinScore = 0;
479+
this.uiElementsManager.updateScore(this.coinScore);
480+
this.storage.saveScore(StoredDataType.CurrentScore, 0);
481+
}
482+
483+
private restart() {
484+
this.gameStatus = GameStatus.Playing;
485+
this.resetGameObjects();
486+
this.createGameObjects();
487+
this.uiElementsManager.hideGameOverScreen();
488+
this.uiElementsManager.showUiElements();
445489
}
446490
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { GameHTMLElementsRefs } from './types';
2+
3+
export class GameUiElementsManager {
4+
public constructor(private readonly elementsRefs: GameHTMLElementsRefs) {}
5+
6+
public shrinkCanvas() {
7+
this.elementsRefs.canvas.width = this.elementsRefs.canvas.clientWidth;
8+
this.elementsRefs.canvas.height = this.elementsRefs.canvas.clientHeight;
9+
}
10+
11+
public updateScore(score: number) {
12+
this.elementsRefs.scoreText.innerText = String(score);
13+
}
14+
15+
public showGameOverScreen(currentScore: number, bestScore: number) {
16+
this.elementsRefs.gameOver.currentScore.innerText = `CURRENT SCORE: ${currentScore}`;
17+
this.elementsRefs.gameOver.bestScore.innerText = `BEST SCORE: ${bestScore}`;
18+
this.elementsRefs.gameOver.screen.style.display = 'flex';
19+
}
20+
21+
public hideGameOverScreen() {
22+
this.elementsRefs.gameOver.screen.style.display = 'none';
23+
this.elementsRefs.gameOver.currentScore.innerText = `CURRENT SCORE: 0`;
24+
this.elementsRefs.gameOver.bestScore.innerText = `BEST SCORE: 0`;
25+
}
26+
27+
public hideUiElements() {
28+
for (const element of this.elementsRefs.userIIElements) {
29+
(element as HTMLElement).style.display = 'none';
30+
}
31+
}
32+
33+
public showUiElements() {
34+
for (const element of this.elementsRefs.userIIElements) {
35+
(element as HTMLElement).style.display = 'flex';
36+
}
37+
}
38+
}

games/running-ball-babylonjs/src/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ function main() {
55
const args: GameHTMLElementsRefs = {
66
canvas: document.getElementById('game-canvas') as HTMLCanvasElement,
77
scoreText: document.getElementById('game-score') as HTMLCanvasElement,
8+
userIIElements: document.getElementsByClassName('user-ui'),
9+
restartButtons: document.getElementsByClassName('restart-btn'),
810
gameOver: {
911
screen: document.getElementById('game-over-screen') as HTMLDivElement,
1012
currentScore: document.getElementById('game-over-score-current') as HTMLDivElement,
@@ -16,4 +18,4 @@ function main() {
1618
game.init();
1719
}
1820

19-
main();
21+
window.onload = main;

games/running-ball-babylonjs/src/style.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ body {
8181
.game-canvas {
8282
width: 100%;
8383
height: 100%;
84+
outline: none;
8485
}
8586

8687
.game-over-screen {

games/running-ball-babylonjs/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface Size3D {
77
export interface GameHTMLElementsRefs {
88
canvas: HTMLCanvasElement;
99
scoreText: HTMLElement;
10+
userIIElements: HTMLCollection;
11+
restartButtons: HTMLCollection;
1012
gameOver: {
1113
screen: HTMLDivElement;
1214
currentScore: HTMLSpanElement;

0 commit comments

Comments
 (0)