Last active
November 23, 2023 02:33
-
-
Save lewdev/a166c32dad5353d4114c3625688f0b85 to your computer and use it in GitHub Desktop.
π Tank Movement in JavaScript Canvas
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <style>html,body{background:#222;display:flex;justify-content:center;align-items:center;height:100%;margin:0}#c{background:white;max-height:100%;max-width:100%}</style><canvas id=c width=1080 height=1080></canvas><script>x=c.getContext`2d`,lfr=1,S=Math.sin,C=Math.cos,T=Math.tan,R=(r,g,b,a=1)=>`rgba(${r|0},${g|0},${b|0},${a})`,t=f=nfm=0; | |
| const vec2 = (x,y=x)=>new Vec2(x, y); | |
| class Vec2 { | |
| constructor(x = 0, y = 0) { this.x = x; this.y = y; } | |
| copy() { return vec2(this.x, this.y); } | |
| scale(s) { return vec2(this.x * s, this.y * s); } | |
| add(v) { return vec2(this.x + v.x, this.y + v.y); } | |
| subtract(v) { return vec2(this.x - v.x, this.y - v.y); } | |
| move(a, l) { return vec2(this.x + l * Math.sin(a), this.y - l * Math.cos(a)); } | |
| setAngle(a=0, length=1) { this.x = length*Math.sin(a); this.y = length*Math.cos(a); return this; } | |
| } | |
| class Speed { | |
| constructor(maxSpeed = 1, direction = 0) { | |
| this.maxSpeed = maxSpeed; | |
| this.speed = direction * maxSpeed; | |
| } | |
| move(direction) { | |
| this.speed = direction * this.maxSpeed; | |
| } | |
| copy() { | |
| const { maxSpeed, speed } = this; | |
| const direction = speed < 0 ? -1 : speed > 0 ? 1 : 0; | |
| return new Speed(maxSpeed, direction); | |
| } | |
| } | |
| class Timer { | |
| constructor(timeLeft) { this.time = timeLeft == undefined ? undefined : time + timeLeft; this.setTime = timeLeft; } | |
| set(timeLeft=0) { this.time = time + timeLeft; this.setTime = timeLeft; } | |
| unset() { this.time = undefined; } | |
| elapsed() { return time > this.time; } | |
| getPercent() { return this.isSet()? percent(this.time - time, this.setTime, 0) : 0; } | |
| } | |
| const TILE_SIZE = 100; | |
| const HALF_TILE = TILE_SIZE / 2; | |
| let globalScale = 1; | |
| let time = tDiff = 0; | |
| let SCREEN_SIZE = vec2(1080); | |
| let cameraPos = null; | |
| let objList = []; | |
| const TANK_COLOR = "blue"; | |
| const TANK_SIZE = vec2(80, 100); | |
| const TANK_SPEED = new Speed(300); //per second | |
| const TANK_ROTATE_SPEED = new Speed(3); //per second | |
| const TANK_FIRING_RATE = 1; // per second | |
| const BULLET_SIZE = vec2(10, 10); | |
| const BULLET_SPEED = new Speed(500, 1); | |
| const MAP_DIMENSIONS = vec2(50, 50); | |
| const MAP_SIZE = MAP_DIMENSIONS.scale(TILE_SIZE); | |
| const rect = (pos, size, direction, center, color, s = 1) => { | |
| const scale = s * globalScale; | |
| const relativePos = pos.scale(scale).subtract(cameraPos.scale(scale)).add(SCREEN_SIZE.scale(.5)) | |
| const scaledSize = size.scale(scale); | |
| const halfSize = scaledSize.scale(.5); | |
| if (relativePos.x < -scaledSize.x || relativePos.y < -scaledSize.y | |
| || relativePos.x > SCREEN_SIZE.x + scaledSize.x || relativePos.y > SCREEN_SIZE.y + scaledSize.y) return; | |
| x.fillStyle = color; | |
| x.save(); | |
| x.translate(relativePos.x, relativePos.y); | |
| x.rotate(direction); | |
| x.fillRect(-halfSize.x, -halfSize.y, scaledSize.x, scaledSize.y); | |
| x.restore(); | |
| }; | |
| class Object { | |
| constructor(pos, size, angle = 0, center, color = "gray", moveSpeed, rotateSpeed) { | |
| this.pos = pos; | |
| this.size = size; | |
| this.angle = angle; | |
| this.center = center || size.scale(.5); | |
| this.color = color; | |
| this.moveSpeed = moveSpeed || new Speed; | |
| this.rotateSpeed = rotateSpeed || new Speed; | |
| } | |
| move(direction) { this.moveSpeed.move(direction); } | |
| rotate(direction) { this.rotateSpeed.move(direction); } | |
| update() { | |
| if (this.moveSpeed.speed) this.pos = this.pos.move(this.angle, this.moveSpeed.speed * tDiff); | |
| if (this.rotateSpeed.speed) this.angle = (this.angle + this.rotateSpeed.speed * tDiff); | |
| } | |
| render() { | |
| const { pos, size, angle, center, color } = this; | |
| rect(pos, size, angle, center, color); | |
| } | |
| } | |
| class Bullet extends Object { | |
| constructor(pos, angle) { | |
| super(pos, BULLET_SIZE, angle, 0, "red", BULLET_SPEED.copy()); | |
| } | |
| update() { | |
| super.update(); | |
| //handle collision | |
| const { pos } = this; | |
| this.delete = pos.x < -HALF_TILE || pos.y < -HALF_TILE || pos.y > MAP_SIZE.y || pos.x > MAP_SIZE.x; | |
| } | |
| } | |
| class Tank extends Object { | |
| constructor(pos, angle) { | |
| super(pos, TANK_SIZE, angle, null, TANK_COLOR, TANK_SPEED.copy(), TANK_ROTATE_SPEED.copy()); | |
| this.lastFired = new Timer(TANK_FIRING_RATE); | |
| this.hp = 5; | |
| } | |
| update() { | |
| super.update(); | |
| if (this.isFiring && this.lastFired.elapsed()) { | |
| this.lastFired.set(TANK_FIRING_RATE); | |
| objList.push(new Bullet(this.pos.move(this.angle, TANK_SIZE.y / 2), this.angle)); | |
| this.isFiring = false; | |
| } | |
| } | |
| } | |
| class EnemyTank extends Tank { | |
| constructor(pos) { | |
| super(pos, 0); | |
| this.color = "orange"; | |
| } | |
| } | |
| const drawMap = () => { | |
| for (i = MAP_DIMENSIONS.x; i--;) for (j = MAP_DIMENSIONS.y; j--;) | |
| rect(vec2(i, j).scale(TILE_SIZE), vec2(TILE_SIZE * .9), 0, 0, "#AFA") | |
| }; | |
| const player = new Tank(vec2(10, 10).scale(TILE_SIZE)); | |
| objList.push(player); | |
| for (i = 10; i--;) objList.push(new EnemyTank(vec2(i * 3 + 3, 5).scale(TILE_SIZE))); | |
| u=r=d=l=X=Y=0; | |
| onkeydown=onkeyup=e=> { | |
| this[`lurd${"*".repeat(24)}l**r************l*d***u**u`[e.which-37]]=e.type[5] | |
| if (e.code === "Space" && e.type[5]) player.isFiring = true; | |
| }; | |
| window.addEventListener("wheel", e => { | |
| const direction = (e.detail < 0) ? 1 : (e.wheelDelta > 0) ? 1 : -1; | |
| globalScale = Math.min(3, Math.max(0.2, globalScale + direction * .2)); | |
| }); | |
| z=t=>{ c.width|=0 //loop and clear canvas | |
| tDiff = t - time; | |
| time = t; | |
| // player controls | |
| player.move(u ? 1 : d ? -1 : 0); | |
| player.rotate(r ? 1 : l ? -1 : 0); | |
| // update | |
| objList = objList.filter(o => !o.delete); | |
| cameraPos = player.pos.copy(); | |
| // render | |
| drawMap(); | |
| for (o of objList) o.update(), o.render(); | |
| };loop=fTime=>{requestAnimationFrame(loop);if(lfr&&fTime<nfm-2)return;nfm=Math.max(nfm+1e3/60,fTime);t=f/60;if(t*60|0!=f++)t+=1e-6;z(t)}; | |
| onload = _ => loop()</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment