瀏覽代碼

Rain effect + optimisations

Pabloader 2 年之前
父節點
當前提交
87032574e2
共有 9 個文件被更改,包括 181 次插入73 次删除
  1. 6
    38
      css/style.css
  2. 2
    0
      index.html
  3. 40
    2
      js/bullet.js
  4. 46
    13
      js/cloud.js
  5. 17
    7
      js/entity.js
  6. 29
    3
      js/game.js
  7. 20
    9
      js/shooting_pad.js
  8. 19
    0
      js/utils.js
  9. 2
    1
      todo.md

+ 6
- 38
css/style.css 查看文件

@@ -13,9 +13,8 @@ html, body {
13 13
     font-family: arial;
14 14
 }
15 15
 
16
-#root {
17
-    background: rgb(31, 31, 31);
18
-    position: relative;
16
+#root, #canvas {
17
+    position: absolute;
19 18
     margin: 0;
20 19
     padding: 0;
21 20
     width: 100%;
@@ -24,6 +23,10 @@ html, body {
24 23
     overflow: hidden;
25 24
 }
26 25
 
26
+#canvas {
27
+    background: rgb(31, 31, 31);
28
+}
29
+
27 30
 #root div {
28 31
     transform: translate(-50%, -50%);
29 32
     position: absolute;
@@ -37,23 +40,6 @@ html, body {
37 40
     animation-direction: alternate;
38 41
 }
39 42
 
40
-.shooting-pad {
41
-    background: url('../img/shooting-pad.svg') no-repeat;
42
-    background-size: 100%;
43
-    animation-iteration-count: 10;
44
-    animation-duration: 200ms;
45
-}
46
-
47
-.bullet {
48
-    background: yellow;
49
-    width: 1px !important;
50
-}
51
-
52
-.raindrop {
53
-    background: cyan;
54
-    width: 1px !important;
55
-}
56
-
57 43
 .result {
58 44
     left: 50%;
59 45
     top: 50%;
@@ -92,22 +78,4 @@ html, body {
92 78
         transform: translate(-50%) scale(0.2);
93 79
         opacity: 0.1;
94 80
     }
95
-}
96
-
97
-@keyframes blink {
98
-    0% {
99
-        opacity: 1;
100
-    }
101
-    1% {
102
-        opacity: 0;
103
-    }
104
-    50% {
105
-        opacity: 0;
106
-    }
107
-    51% {
108
-        opacity: 1;
109
-    }
110
-    100% {
111
-        opacity: 1;
112
-    }
113 81
 }

+ 2
- 0
index.html 查看文件

@@ -6,9 +6,11 @@
6 6
         <meta charset="utf-8">
7 7
     </head>
8 8
     <body>
9
+        <canvas id="canvas" width="1280" height="720"></canvas>
9 10
         <div id="root">
10 11
             <section id="lives"></section>
11 12
         </div>
13
+        <script src="https://cdn.jsdelivr.net/gh/josephg/noisejs/perlin.js"></script>
12 14
         <script src="js/utils.js"></script>
13 15
         <script src="js/entity.js"></script>
14 16
         <script src="js/bullet.js"></script>

+ 40
- 2
js/bullet.js 查看文件

@@ -1,7 +1,7 @@
1 1
 class Bullet extends Entity {
2 2
     constructor(x, y) {
3 3
         super(x, y, 0.001, 0.02);
4
-        this.element.className = 'bullet';
4
+        this.color = 'yellow';
5 5
     }
6 6
 
7 7
     update() {
@@ -24,7 +24,7 @@ class Bullet extends Entity {
24 24
 class RainDrop extends Bullet {
25 25
     constructor(x, y) {
26 26
         super(x, y);
27
-        this.element.className = 'raindrop';
27
+        this.color = 'cyan';
28 28
     }
29 29
 
30 30
     update() {
@@ -38,4 +38,42 @@ class RainDrop extends Bullet {
38 38
             }
39 39
         }
40 40
     }
41
+}
42
+
43
+const currentAngle = () => {
44
+    const x = Date.now() / 10000;
45
+    const n = noise.simplex2(x, 0);
46
+
47
+    return mapNumber(n, -1, 1, 45, 135);
48
+};
49
+
50
+
51
+class BackgroundRaindrop extends Bullet {
52
+    constructor(x, y, speed) {
53
+        super(x, y);
54
+        this.speed = speed;
55
+    }
56
+
57
+    draw() {
58
+        CTX.save();
59
+        const { x, y, w, h } = this.getScreenCoords();
60
+        CTX.translate(x, y);
61
+        CTX.rotate(toRadians(currentAngle() - 90));
62
+        CTX.fillStyle = `rgba(200, 255, 255, 0.2)`;
63
+        CTX.fillRect(0, 0, w, h);
64
+        CTX.restore();
65
+    }
66
+
67
+    update() {
68
+        const cs = Math.cos(toRadians(currentAngle()));
69
+        const sn = Math.sin(toRadians(currentAngle()));
70
+
71
+        this.x += this.speed * cs;
72
+        this.y += this.speed * sn;
73
+
74
+        if (this.y > 1.1) {
75
+            this.y = randRange(-1, -0.1);
76
+            this.x = randRange(-0.5, 1.5);
77
+        }
78
+    }
41 79
 }

+ 46
- 13
js/cloud.js 查看文件

@@ -6,19 +6,52 @@ class Cloud extends Entity {
6 6
 
7 7
     constructor(...args) {
8 8
         super(...args);
9
-        this.element.className = 'cloud';
10
-
11
-        for (let i = 0; i < 10; i++) {
12
-            const cloudParticle = document.createElement('div');
13
-            cloudParticle.className = 'cloud-particle';
14
-            cloudParticle.style.width = `${randRange(50, 80)}%`;
15
-            cloudParticle.style.height = `${randRange(60, 100)}%`;
16
-            cloudParticle.style.left = `${randRange(2, 98)}%`;
17
-            cloudParticle.style.top = `${randRange(2, 98)}%`;
18
-            cloudParticle.style.animationDelay = `${randRange(-10, 0)}s`;
19
-            cloudParticle.style.animationDuration = `${randRange(0.5, 3)}s`;
20
-
21
-            this.element.appendChild(cloudParticle);
9
+
10
+        this.particles = new Set();
11
+
12
+        for (let i = 0; i < 3; i++) {
13
+            const cloudParticle = {
14
+                w: randRange(0.3, 0.8),
15
+                h: randRange(0.3, 0.8),
16
+                x: randRange(0.2, 0.8),
17
+                y: randRange(0.2, 0.8),
18
+                r: randRange(-Math.PI, Math.PI),
19
+                n: randRange(-10000, 10000),
20
+            }
21
+
22
+            this.particles.add(cloudParticle);
23
+        }
24
+    }
25
+
26
+    draw() {
27
+        const { x, y, w, h } = this.getScreenCoords();
28
+
29
+        for (let particle of this.particles) {
30
+            const px = x + particle.x * w;
31
+            const py = y + particle.y * h;
32
+            const pw = particle.w * w;
33
+            const ph = particle.h * w;
34
+            const now = Date.now() / 1000;
35
+
36
+            const a = mapNumber(
37
+                noise.simplex3(particle.n, now / 2, 0),
38
+                -1, 1,
39
+                0, 0.7,
40
+            );
41
+            const r = mapNumber(
42
+                noise.simplex3(particle.n, now / 10, 1),
43
+                -1, 1,
44
+                0, Math.PI,
45
+            );
46
+            const z = mapNumber(
47
+                noise.simplex3(particle.n, now / 2, 2),
48
+                -1, 1,
49
+                0.5, 1.5,
50
+            );
51
+            CTX.fillStyle = `rgba(255, 255, 255, ${a})`;
52
+            CTX.beginPath();
53
+            CTX.ellipse(px, py, pw / 2 * z, ph / 2 * z, particle.r + r, 0, 2 * Math.PI);
54
+            CTX.fill();
22 55
         }
23 56
     }
24 57
 

+ 17
- 7
js/entity.js 查看文件

@@ -5,22 +5,32 @@ class Entity {
5 5
         this.w = w;
6 6
         this.h = h;
7 7
         this.element = document.createElement('div');
8
-        ROOT.appendChild(this.element);
9
-        this.draw();
10 8
         this.alive = true;
11 9
     }
12 10
 
11
+    getScreenCoords() {
12
+        return {
13
+            x: (this.x - this.w / 2) * CANVAS.width,
14
+            y: (this.y - this.h / 2) * CANVAS.height,
15
+            w: this.w * CANVAS.width,
16
+            h: this.h * CANVAS.height,
17
+        }
18
+    }
19
+
13 20
     draw() {
14 21
         if (this.alive) {
15
-            this.element.style.left = `${this.x * 100}%`;
16
-            this.element.style.top = `${this.y * 100}%`;
17
-            this.element.style.width = `${this.w * 100}%`;
18
-            this.element.style.height = `${this.h * 100}%`;
22
+            const { x, y, w, h } = this.getScreenCoords();
23
+
24
+            if (this.image) {
25
+                CTX.drawImage(this.image, x, y, w, h);
26
+            } else {
27
+                CTX.fillStyle = this.color ?? 'red';
28
+                CTX.fillRect(x, y, w, h);
29
+            }
19 30
         }
20 31
     }
21 32
 
22 33
     destroy() {
23
-        this.element.remove();
24 34
         this.alive = false;
25 35
     }
26 36
 

+ 29
- 3
js/game.js 查看文件

@@ -1,10 +1,15 @@
1 1
 const $ = (s) => document.querySelector(s);
2 2
 const $$ = (s) => Array.from(document.querySelectorAll(s));
3 3
 const ROOT = $('#root');
4
+const CANVAS = $('#canvas');
5
+const CTX = CANVAS.getContext('2d');
4 6
 const X_BOUND = 0.05;
7
+const GAME_OVER_BOUND = 0.9;
8
+const MAX_RAINDROPS = 100;
5 9
 
6 10
 const clouds = new Set();
7 11
 const bullets = new Set();
12
+const raindrops = new Set();
8 13
 let shootingPad = new ShootingPad();
9 14
 let running = true;
10 15
 let direction = 1;
@@ -12,12 +17,25 @@ let direction = 1;
12 17
 const initCloudField = () => {
13 18
     for (let y = 0; y < 6; y++) {
14 19
         for (let x = 0; x < 11; x++) {
15
-            const cloud = new Cloud(X_BOUND + 0.06 * x, 0.04 + 0.04 * y, 0.03, 0.02);
20
+            const cloud = new Cloud(X_BOUND + 0.06 * x, 0.04 + 0.06 * y, 0.03, 0.02);
16 21
             clouds.add(cloud);
17 22
         }
18 23
     }
19 24
 }
20 25
 
26
+const initRainEffect = () => {
27
+    noise.seed(Math.random());
28
+    for (let i = 0; i < MAX_RAINDROPS; i++) {
29
+        const raindrop = new BackgroundRaindrop(
30
+            randRange(-0.5, 1.5),
31
+            randRange(-1, -0.1),
32
+            randRange(0.01, 0.03),
33
+        );
34
+
35
+        raindrops.add(raindrop);
36
+    }
37
+}
38
+
21 39
 const start = () => {
22 40
     $('.result')?.remove();
23 41
 
@@ -46,16 +64,18 @@ const gameOver = (result) => {
46 64
 
47 65
 const loop = () => {
48 66
     if (!running) return;
67
+    document.title = `${fps()} FPS | CloudInvaders | OLC Codejam 2022`;
68
+    CTX.clearRect(0, 0, CANVAS.width, CANVAS.height);
49 69
 
50 70
     let directionUpdated = false;
51 71
     let isOver = false;
52 72
     Array.from(clouds).forEach(c => {
53 73
         c.update(direction);
54 74
         c.draw();
55
-        if (c.x < X_BOUND - 0.01 || c.x > 1.01 - X_BOUND) {
75
+        if (c.x < X_BOUND || c.x > 1 - X_BOUND) {
56 76
             directionUpdated = true;
57 77
         }
58
-        if (c.y >= 0.95) {
78
+        if (c.y >= GAME_OVER_BOUND) {
59 79
             isOver = true;
60 80
         }
61 81
         if (!c.alive) {
@@ -80,6 +100,11 @@ const loop = () => {
80 100
         }
81 101
     });
82 102
 
103
+    raindrops.forEach(d => {
104
+        d.update();
105
+        d.draw();
106
+    });
107
+
83 108
     shootingPad.update();
84 109
     shootingPad.draw();
85 110
 
@@ -96,4 +121,5 @@ document.addEventListener('keypress', () => {
96 121
     }
97 122
 });
98 123
 
124
+initRainEffect();
99 125
 start();

+ 20
- 9
js/shooting_pad.js 查看文件

@@ -7,28 +7,31 @@ document.addEventListener('keyup', (e) => {
7 7
     keys[e.key] = false;
8 8
 });
9 9
 
10
+let blink = false;
11
+
12
+setInterval(() => blink = !blink, 200);
13
+
10 14
 const MAX_LIVES = 3;
15
+const INVULNERABILITY_INTERVAL = 2000;
11 16
 
12 17
 class ShootingPad extends Entity {
13 18
     constructor() {
14 19
         super(0.5, 0.95, 0.04, 0.03);
15
-        this.element.className = 'shooting-pad';
20
+        const image = new Image();
21
+        image.src = 'img/shooting-pad.svg';
22
+        image.onload = () => this.image = image;
23
+
16 24
         this.lastShootTime = Date.now();
17 25
         this.invulnerable = false;
18 26
         this.lives = MAX_LIVES;
19 27
         this.initLives();
20
-
21
-        this.element.addEventListener('animationend', () => {
22
-            this.element.style.animationName = null;
23
-            this.invulnerable = false;
24
-        });
25 28
     }
26 29
 
27 30
     update() {
28
-        if (keys['ArrowLeft']) {
31
+        if (keys['ArrowLeft'] || keys['a']) {
29 32
             this.x -= VELOCITY[0] * 2;
30 33
         }
31
-        if (keys['ArrowRight']) {
34
+        if (keys['ArrowRight'] || keys['d']) {
32 35
             this.x += VELOCITY[0] * 2;
33 36
         }
34 37
         if (this.x < X_BOUND) {
@@ -44,6 +47,12 @@ class ShootingPad extends Entity {
44 47
         }
45 48
     }
46 49
 
50
+    draw() {
51
+        if (!this.invulnerable || blink) {
52
+            super.draw();
53
+        }
54
+    }
55
+
47 56
     initLives() {
48 57
         $('#lives').textContent = `× ${this.lives}`;
49 58
     }
@@ -58,7 +67,9 @@ class ShootingPad extends Entity {
58 67
             return gameOver('Game over');
59 68
         } else {
60 69
             this.invulnerable = true;
61
-            this.element.style.animationName = 'blink';
70
+            setTimeout(() => {
71
+                this.invulnerable = false;
72
+            }, INVULNERABILITY_INTERVAL);
62 73
         }
63 74
     }
64 75
 }

+ 19
- 0
js/utils.js 查看文件

@@ -1 +1,20 @@
1 1
 const randRange = (low, high) => Math.random() * (high - low) + low;
2
+const toRadians = (degrees) => degrees * Math.PI / 180;
3
+const mapNumber = (value, fromLow, fromHigh, toLow, toHigh) => {
4
+    const normalized = (value - fromLow) / (fromHigh - fromLow);
5
+    return normalized * (toHigh - toLow) + toLow;
6
+};
7
+
8
+let fps_value = 0;
9
+let fps_frames = 0;
10
+let fps_last_capture = Date.now();
11
+const fps = () => {
12
+    fps_frames++;
13
+    if (Date.now() - fps_last_capture >= 500) {
14
+        fps_value = fps_frames * 2;
15
+        fps_frames = 0;
16
+        fps_last_capture = Date.now();
17
+    }
18
+
19
+    return fps_value;
20
+}

+ 2
- 1
todo.md 查看文件

@@ -3,7 +3,8 @@
3 3
 - [x] lives
4 4
 - [x] shooting platform sprite
5 5
 - [ ] clouds should strike lightnings sometimes
6
-- [ ] bg rain effect
6
+- [x] bg rain effect
7
+- [ ] animated gif for clouds
7 8
 - [ ] boss?
8 9
 - [ ] umbrella shields
9 10
 - [ ] music & sounds

Loading…
取消
儲存