Browse Source

Rain effect + optimisations

Pabloader 2 years ago
parent
commit
87032574e2
9 changed files with 181 additions and 73 deletions
  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 View File

13
     font-family: arial;
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
     margin: 0;
18
     margin: 0;
20
     padding: 0;
19
     padding: 0;
21
     width: 100%;
20
     width: 100%;
24
     overflow: hidden;
23
     overflow: hidden;
25
 }
24
 }
26
 
25
 
26
+#canvas {
27
+    background: rgb(31, 31, 31);
28
+}
29
+
27
 #root div {
30
 #root div {
28
     transform: translate(-50%, -50%);
31
     transform: translate(-50%, -50%);
29
     position: absolute;
32
     position: absolute;
37
     animation-direction: alternate;
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
 .result {
43
 .result {
58
     left: 50%;
44
     left: 50%;
59
     top: 50%;
45
     top: 50%;
92
         transform: translate(-50%) scale(0.2);
78
         transform: translate(-50%) scale(0.2);
93
         opacity: 0.1;
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 View File

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

+ 40
- 2
js/bullet.js View File

1
 class Bullet extends Entity {
1
 class Bullet extends Entity {
2
     constructor(x, y) {
2
     constructor(x, y) {
3
         super(x, y, 0.001, 0.02);
3
         super(x, y, 0.001, 0.02);
4
-        this.element.className = 'bullet';
4
+        this.color = 'yellow';
5
     }
5
     }
6
 
6
 
7
     update() {
7
     update() {
24
 class RainDrop extends Bullet {
24
 class RainDrop extends Bullet {
25
     constructor(x, y) {
25
     constructor(x, y) {
26
         super(x, y);
26
         super(x, y);
27
-        this.element.className = 'raindrop';
27
+        this.color = 'cyan';
28
     }
28
     }
29
 
29
 
30
     update() {
30
     update() {
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 View File

6
 
6
 
7
     constructor(...args) {
7
     constructor(...args) {
8
         super(...args);
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 View File

5
         this.w = w;
5
         this.w = w;
6
         this.h = h;
6
         this.h = h;
7
         this.element = document.createElement('div');
7
         this.element = document.createElement('div');
8
-        ROOT.appendChild(this.element);
9
-        this.draw();
10
         this.alive = true;
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
     draw() {
20
     draw() {
14
         if (this.alive) {
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
     destroy() {
33
     destroy() {
23
-        this.element.remove();
24
         this.alive = false;
34
         this.alive = false;
25
     }
35
     }
26
 
36
 

+ 29
- 3
js/game.js View File

1
 const $ = (s) => document.querySelector(s);
1
 const $ = (s) => document.querySelector(s);
2
 const $$ = (s) => Array.from(document.querySelectorAll(s));
2
 const $$ = (s) => Array.from(document.querySelectorAll(s));
3
 const ROOT = $('#root');
3
 const ROOT = $('#root');
4
+const CANVAS = $('#canvas');
5
+const CTX = CANVAS.getContext('2d');
4
 const X_BOUND = 0.05;
6
 const X_BOUND = 0.05;
7
+const GAME_OVER_BOUND = 0.9;
8
+const MAX_RAINDROPS = 100;
5
 
9
 
6
 const clouds = new Set();
10
 const clouds = new Set();
7
 const bullets = new Set();
11
 const bullets = new Set();
12
+const raindrops = new Set();
8
 let shootingPad = new ShootingPad();
13
 let shootingPad = new ShootingPad();
9
 let running = true;
14
 let running = true;
10
 let direction = 1;
15
 let direction = 1;
12
 const initCloudField = () => {
17
 const initCloudField = () => {
13
     for (let y = 0; y < 6; y++) {
18
     for (let y = 0; y < 6; y++) {
14
         for (let x = 0; x < 11; x++) {
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
             clouds.add(cloud);
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
 const start = () => {
39
 const start = () => {
22
     $('.result')?.remove();
40
     $('.result')?.remove();
23
 
41
 
46
 
64
 
47
 const loop = () => {
65
 const loop = () => {
48
     if (!running) return;
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
     let directionUpdated = false;
70
     let directionUpdated = false;
51
     let isOver = false;
71
     let isOver = false;
52
     Array.from(clouds).forEach(c => {
72
     Array.from(clouds).forEach(c => {
53
         c.update(direction);
73
         c.update(direction);
54
         c.draw();
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
             directionUpdated = true;
76
             directionUpdated = true;
57
         }
77
         }
58
-        if (c.y >= 0.95) {
78
+        if (c.y >= GAME_OVER_BOUND) {
59
             isOver = true;
79
             isOver = true;
60
         }
80
         }
61
         if (!c.alive) {
81
         if (!c.alive) {
80
         }
100
         }
81
     });
101
     });
82
 
102
 
103
+    raindrops.forEach(d => {
104
+        d.update();
105
+        d.draw();
106
+    });
107
+
83
     shootingPad.update();
108
     shootingPad.update();
84
     shootingPad.draw();
109
     shootingPad.draw();
85
 
110
 
96
     }
121
     }
97
 });
122
 });
98
 
123
 
124
+initRainEffect();
99
 start();
125
 start();

+ 20
- 9
js/shooting_pad.js View File

7
     keys[e.key] = false;
7
     keys[e.key] = false;
8
 });
8
 });
9
 
9
 
10
+let blink = false;
11
+
12
+setInterval(() => blink = !blink, 200);
13
+
10
 const MAX_LIVES = 3;
14
 const MAX_LIVES = 3;
15
+const INVULNERABILITY_INTERVAL = 2000;
11
 
16
 
12
 class ShootingPad extends Entity {
17
 class ShootingPad extends Entity {
13
     constructor() {
18
     constructor() {
14
         super(0.5, 0.95, 0.04, 0.03);
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
         this.lastShootTime = Date.now();
24
         this.lastShootTime = Date.now();
17
         this.invulnerable = false;
25
         this.invulnerable = false;
18
         this.lives = MAX_LIVES;
26
         this.lives = MAX_LIVES;
19
         this.initLives();
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
     update() {
30
     update() {
28
-        if (keys['ArrowLeft']) {
31
+        if (keys['ArrowLeft'] || keys['a']) {
29
             this.x -= VELOCITY[0] * 2;
32
             this.x -= VELOCITY[0] * 2;
30
         }
33
         }
31
-        if (keys['ArrowRight']) {
34
+        if (keys['ArrowRight'] || keys['d']) {
32
             this.x += VELOCITY[0] * 2;
35
             this.x += VELOCITY[0] * 2;
33
         }
36
         }
34
         if (this.x < X_BOUND) {
37
         if (this.x < X_BOUND) {
44
         }
47
         }
45
     }
48
     }
46
 
49
 
50
+    draw() {
51
+        if (!this.invulnerable || blink) {
52
+            super.draw();
53
+        }
54
+    }
55
+
47
     initLives() {
56
     initLives() {
48
         $('#lives').textContent = `× ${this.lives}`;
57
         $('#lives').textContent = `× ${this.lives}`;
49
     }
58
     }
58
             return gameOver('Game over');
67
             return gameOver('Game over');
59
         } else {
68
         } else {
60
             this.invulnerable = true;
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 View File

1
 const randRange = (low, high) => Math.random() * (high - low) + low;
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 View File

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

Loading…
Cancel
Save