前言
? ? ? ? ? Acwing Web应用课 拳皇 中期项目!
一、原理简介
- 一个物体移动的基本逻辑
浏览器默认每秒刷新 60 次,每次单独计算一下物体新的位置,然后刷新出来 - 运动方式
- 平面移动
- 二维运动:坐标(x, y),宽高,vx:x 方向运动速度,vy:y方向运动速度
- 每次水平方向的位置更新: x = x0 + vx * t
- 游戏设计思路
二、代码实现
面向对象写法
将每一个元素都写成一个 class ,方便维护,整个游戏为一个 class 。
创建游戏窗口类
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拳皇</title>
<link rel="stylesheet" href="/KOF/static/css/base.css">
<script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script>
</head>
<body>
<div id="kof"></div>
<script type="module">
import { KOF } from "/KOF/static/js/base.js";
let kof = new KOF('kof');
</script>
</body>
</html>
css
#kof {
width: 1280px;
height: 720px;
background-image: url(/KOF/static/images/background/0.gif);
background-size: 200% 100%;
background-position: top;
}
js
class KOF {
constructor(id) {
this.$kof = $('#' + id);
}
}
export {
KOF
}
实现效果如下
整个游戏实现
1. 创建 ac_game_object 类
-
为什么:game_map ,player 等对象都需要在每秒画
60
60
60 次以达到移动动画的效果,所以我们创建一个共同的基类 ac_game_object ,game_map ,player 等对象继承 ac_game_object 这个基类即可。 -
作用:实现 3 个元素每秒钟都刷新 60 次 -
js 代码实现
let AC_GAME_OBJECTS = [];
class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this);
this.timedelta = 0;
this.has_call_start = false;
}
start() {
}
update() {
}
destroy() {
for (let i in AC_GAME_OBJECTS) {
if (AC_GAME_OBJECTS[i] == this) {
AC_GAME_OBJECTS.splice(i, i);
break;
}
}
}
}
let last_timestamp;
let AC_GAME_OBJECTS_FRAME = (timestamp) => {
for (let obj of AC_GAME_OBJECTS) {
if (!obj.has_call_start) {
obj.start();
obj.has_call_start = true;
} else {
obj.timedelta = timestamp - last_timestamp;
obj.update();
}
}
last_timestamp = timestamp;
requestAnimationFrame(AC_GAME_OBJECTS_FRAME);
}
requestAnimationFrame(AC_GAME_OBJECTS_FRAME);
export {
AcGameObject
}
2. 定义 game_map 对象
import { AcGameObject } from '/static/js/ac_game_object/base.js';
import { Controller } from "/static/js/controller/base.js"
class GameMap extends AcGameObject {
constructor(root) {
super();
this.root = root;
this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>');
this.ctx = this.$canvas[0].getContext('2d');
this.root.$kof.append(this.$canvas);
this.$canvas.focus();
this.controller = new Controller(this.$canvas);
}
start() {
}
update() {
this.render();
}
render() {
this.ctx.fillStyle = 'black';
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
}
export {
GameMap
}
3. 定义 player 对象
1. 实现两个玩家
用两个矩形代表两个玩家
import { AcGameObject } from "/static/js/ac_game_object/base.js";
export class Player extends AcGameObject {
constructor(root, info) {
super();
this.root = root;
this.id = info.id;
this.x = info.x;
this.y = info.y;
this.width = info.width;
this.height = info.height;
this.color = info.color;
this.direction = 1;
this.vx = 0;
this.vy = 0;
this.speedx = 400;
this.speedy = 1000;
this.gravity = 50;
this.ctx = this.root.game_map.ctx;
}
start() {
}
move() {
this.vy += this.gravity;
this.x += this.vx * this.timedelta / 1000;
this.y += this.vy * this.timedelta / 1000;
if (this.y > 500) {
this.y = 500;
this.vy = 0;
}
}
update() {
this.move();
this.render();
}
render() {
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
import { GameMap } from "/static/js/game_map/base.js";
import { Player } from "/static/js/player/base.js"
class KOF {
constructor(id) {
this.$kof = $('#' + id);
this.game_map = new GameMap(this);
this.players = [
new Player(this, {
id: 0,
x: 200,
y: 0,
width: 120,
height: 200,
color: 'blue',
}),
new Player(this, {
id: 1,
x: 900,
y: 0,
width: 120,
height: 200,
color: 'red',
})
];
}
}
export {
KOF
}
4. 定义controller 对象
状态机实现玩家不同状态间的转换
export class Controller {
constructor($canvas) {
this.$canvas = $canvas;
this.pressed_keys = new Set();
this.start();
}
start() {
let outer = this;
this.$canvas.keydown(function (e) {
outer.pressed_keys.add(e.key);
});
this.$canvas.keyup(function (e) {
outer.pressed_keys.delete(e.key);
});
}
}
5. 在 player 对象中添加 update_control 函数
import { AcGameObject } from "/static/js/ac_game_object/base.js";
export class Player extends AcGameObject {
constructor(root, info) {
super();
this.root = root;
this.id = info.id;
this.x = info.x;
this.y = info.y;
this.width = info.width;
this.height = info.height;
this.color = info.color;
this.direction = 1;
this.vx = 0;
this.vy = 0;
this.speedx = 400;
this.speedy = -1000;
this.gravity = 50;
this.ctx = this.root.game_map.ctx;
this.pressed_keys = this.root.game_map.controller.pressed_keys;
this.status = 3;
}
start() {
}
update_move() {
this.vy += this.gravity;
this.x += this.vx * this.timedelta / 1000;
this.y += this.vy * this.timedelta / 1000;
if (this.y > 500) {
this.y = 500;
this.vy = 0;
this.status = 0;
}
}
update_control() {
let w, a, d, space;
if (this.id === 0) {
w = this.pressed_keys.has('w');
a = this.pressed_keys.has('a');
d = this.pressed_keys.has('d');
space = this.pressed_keys.has(' ');
} else {
w = this.pressed_keys.has('ArrowUp');
a = this.pressed_keys.has('ArrowLeft');
d = this.pressed_keys.has('ArrowRight');
space = this.pressed_keys.has('Enter');
}
if (this.status === 0 || this.status === 1) {
if (w) {
if (d) {
this.vx = this.speedx;
} else if (a) {
this.vx = -this.speedx;
} else {
this.vx = 0;
}
this.vy = this.speedy;
this.status = 3;
} else if (d) {
this.vx = this.speedx;
this.status = 1
} else if (a) {
this.vx = -this.speedx;
this.status = 1;
} else {
this.vx = 0;
this.status = 0;
}
}
}
update() {
this.update_control();
this.update_move();
this.render();
}
render() {
this.ctx.fillStyle = this.color;
this.ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
|