前一讲: 创建菜单界面(详细操作步骤)https://www.acwing.com/blog/content/33077/
前端 JS 模块化改造
将原来全局的 JS 定义改造成模块化的形式,缩小作用域,同时防止之后项目变大之后变量重名等。
删除 web.html
中的全局引入文件 game.js
,在 js 代码中声明 script 的 type="module"
,修改后的代码
{% load static %}
<head>
<link rel="stylesheet" href="https://cdn.acwing.com/static/jquery-ui-dist/jquery-ui.min.css">
<script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script>
<link rel="stylesheet" href="{% static 'css/game.css' %}">
</head>
<body style="margin: 0">
<div id="ac_game_12345678"></div>
<script type="module">
import {AcGame} from "{% static 'js/dist/game.js' %}";
$(document).ready(function(){
let ac_gmae = new AcGame("ac_game_12345678");
});
console.log("hello acapp");
</script>
</body>
在总的 zbase.js
中导出 AcGame,在类声明前添加 export 关键字
export class AcGame {}
编写游戏界面逻辑
改完记得执行脚本打包代码
修改 playground/zbase.js
this.$playground = $(`<div class="ac-game-playground"></div>`);
添加样式 game.css
.ac-game-playground {
witdh: 100%;
height: 100%;
user-select: none;
}
实现一个简易的游戏引擎
y 总金句:不要依赖 IDE,导致未来发展很受局限。
之后的所有对象(地图、玩家、技能等)都继承这个游戏引擎对象,拥有里面的处理方法,可以使用父类的,可以自己重载实现。
let AC_GAME_OBJECTS = [];
class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this);
this.has_called_start = false; // 是否执行过 start() | run start()?
this.timedelta = 0; // 两帧之间的时间间隔 ms | time interval of two frames
}
start() { // 只会在第一帧执行一次 | only run once at first frame
}
update() { // 每一帧都会执行一次 | run once every frame
}
on_destroyed() { // 在被销毁前执行一次 | run once before delete
}
destroy() { // 删除该物体 | delete it
this.on_destory();
for (let i = 0; i < AC_GAME_OBJECTS.length; i ++ ) {
if (AC_GAME_OBJECTS[i] === this) {
AC_GAME_OBJECTS.splice(i, 1); // 从 i 开始删除一个 | delete one element from i pos
break;
}
}
}
}
let last_timestamp; // 上一帧的时间戳
// 渲染每一帧之后的回调函数 | render every frame callback function
let AC_GAME_ANIMATION = function(timestamp) {
for (let i = 0; i < AC_GAME_OBJECTS.length; i ++ ) {
let obj = AC_GAME_OBJECTS[i];
if (!obj.has_called_start) {
obj.start();
obj.has_called_start = true;
} else {
obj.timedelta = timestamp - last_timestamp;
obj.update();
}
}
last_timestamp = timestamp;
requestAnimationFrame(AC_GAME_ANIMATION); // 递归渲染 | recursive render
}
requestAnimationFrame(AC_GAME_ANIMATION); // 第一帧图像渲染一次之后,调用回调函数 | run once invoke funtion
右键移动小球
没有效果 Ctrl + F5 刷新页面
game_map/zbase.js
,创建地图
class GameMap extends AcGameObject {
constructor(playground) {
super(); // invoke extends class constructor funtion
this.playground = playground;
this.$canvas = $(`<canvas></canvas>`); // create canvas
this.ctx = this.$canvas[0].getContext('2d'); // 2d canvas
this.ctx.canvas.width = this.playground.width;
this.ctx.canvas.height = this.playground.height;
this.playground.$playground.append(this.$canvas);
}
start() {
}
update() {
this.render(); // 每一帧画一次 | every frame paint once
}
render() {
this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
}
player/zbase.js
,创建玩家
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = 0; // x direction speed, unit is px here
this.vy = 0; // y direction speed
this.move_length = 0; // need to move distance
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1;
}
start() {
if (this.is_me) { // add a mouse listen events only for me
this.add_listening_events();
}
}
add_listening_events() {
let outer = this;
// cancel mouse right click menu
this.playground.game_map.$canvas.on("contextmenu", function() {
return false;
});
// get mouse down x and y coordinate
this.playground.game_map.$canvas.mousedown(function(e) {
if (e.which === 3) { // 3 is mouse down right key
outer.move_to(e.clientX, e.clientY);
}
});
}
get_dist(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
move_to(tx, ty) {
// console.log("move to", tx, ty);
this.move_length = this.get_dist(this.x, this.y, tx, ty);
let angle = Math.atan2(ty - this.y, tx - this.x); // 计算角度 | use arctan function compute angle
this.vx = Math.cos(angle); // compute vx and vy
this.vy = Math.sin(angle);
}
update() {
if (this.move_length < this.eps) { // 如果已经移动到了指定位置就停止移动| if moved to the specific pos, it stops moving.
this.move_length = 0;
this.vx = this.vy = 0;
} else {
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // 计算 timedelta 时间间隔需要真实移动的距离(最后和 move_length 取最小值,防止移动出界) | compute real moving distance in timedelta time intervals.
this.x += this.vx * moved;
this.y += this.vy * moved;
this.move_length -= moved;
// console.log(moved);
}
this.render();
}
render() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
}
总zbase.js
,添加地图和玩家
class AcGamePlayground {
constructor(root) {
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></div>`);
// this.hide(); // 刚开始游戏界面是隐藏的 | playground page is hidden at first
this.root.$ac_game.append(this.$playground);
// 记录界面的宽度和高度,后面经常使用
this.width = this.$playground.width();
this.height = this.$playground.height();
this.game_map = new GameMap(this); // 创建游戏地图 | create game_map
this.players = [];
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true)); // 创建玩家 | create player
this.start();
}
start() {
}
show() { // 打开 playground 界面 | open playground page
this.$playground.show();
}
hide() { // 关闭 playground 界面 | hide playground page
this.$playground.hide();
}
}
到这里实现的效果就是点击鼠标右键小球可以移动到指定位置。
碰撞效果
- 碰撞检测
- 撞击后移
- 粒子效果
JS 是真难调试啊!!!
this.damage_speed 属性少些了一个 a 找了半天…,因为是属性它认为你是新创建的,也不会报错,导致你的速度没有减慢,一直朝着一个方向跑。
完整代码地址:https://git.acwing.com/tonngw/acapp/-/commit/d7db4ec311252ab94265fae7fe31e0c1b0cb8364,核心部分完整代码
playground/zbase.js
class AcGamePlayground {
constructor(root) {
this.root = root;
this.$playground = $(`<div class="ac-game-playground"></div>`);
// this.hide(); // 刚开始游戏界面是隐藏的 | playground page is hidden at first
this.root.$ac_game.append(this.$playground);
// 记录界面的宽度和高度,后面会经常使用
this.width = this.$playground.width();
this.height = this.$playground.height();
this.game_map = new GameMap(this); // 创建游戏地图 | create game_map
this.players = [];
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, "white", this.height * 0.15, true)); // 创建玩家 | create player
// 创建 5 个敌人 | create enemy
for (let i = 0; i < 5; i ++ ) {
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, this.get_random_color(), this.height * 0.15, false));
}
this.start();
}
get_random_color() {
let color = ["blue", "red", "pink", "grey", "green"];
return color[Math.floor(Math.random() * 5)];
}
start() {
}
show() { // 打开 playground 界面 | open playground page
this.$playground.show();
}
hide() { // 关闭 playground 界面 | hide playground page
this.$playground.hide();
}
}
playground/game_map/zbase.js
class GameMap extends AcGameObject {
constructor(playground) {
super(); // invoke extends class constructor funtion
this.playground = playground;
this.$canvas = $(`<canvas></canvas>`); // create canvas
this.ctx = this.$canvas[0].getContext('2d'); // 2d canvas
this.ctx.canvas.width = this.playground.width;
this.ctx.canvas.height = this.playground.height;
this.playground.$playground.append(this.$canvas);
}
start() {
}
update() {
this.render(); // 每一帧画一次 | every frame paint once
}
render() {
this.ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
}
playground/player/zbase.js
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = 0; // x direction speed, unit is px here
this.vy = 0; // y direction speed
this.damage_x = 0; // player is attacked x、y and speed
this.damage_y = 0;
this.damage_speed = 0;
this.move_length = 0; // need to move distance
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1;
this.friction = 0.9; // 撞完之后的摩檫力
this.spent_time = 0; // 冷静时间,冷静之内 AI 不发射炮弹
this.cur_skill = null; // 当前选择的技能,为空就是没选 | current select skill, default null
}
start() {
if (this.is_me) { // if it is itelf, add a mouse listen events only for me
this.add_listening_events();
} else { // else it is enemy, random generate coordinate (tx, ty)
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
}
add_listening_events() {
let outer = this;
// cancel mouse right click menu
this.playground.game_map.$canvas.on("contextmenu", function() {
return false;
});
// get mouse down x and y coordinate
this.playground.game_map.$canvas.mousedown(function(e) {
if (e.which === 3) { // 3 is mouse down right key
outer.move_to(e.clientX, e.clientY);
} else if (e.which === 1) { // 2 is mouse down left key
if (outer.cur_skill === "fireball") { // press mouse left key and Q key shoot firebal
outer.shoot_fireball(e.clientX, e.clientY);
}
outer.cur_skill = null; // clear cur_skill flag after release skill
}
});
$(window).keydown(function(e) { // 添加键盘监听事件 | add keyboard listening events
if (e.which === 81) { // Q key
outer.cur_skill = "fireball";
return false;
}
});
}
shoot_fireball(tx, ty) {
let x = this.x, y = this.y; // fireball coordiate
let radius = this.playground.height * 0.01;
let angle = Math.atan2(ty - this.y, tx - this.x);
let vx = Math.cos(angle), vy = Math.sin(angle);
let color = "orange";
let speed = this.playground.height * 0.5;
let move_length = this.playground.height * 1; // 技能释放的长度 | release skill length
new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, this.playground.height * 0.01);
}
get_dist(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
move_to(tx, ty) {
// console.log("move to", tx, ty);
this.move_length = this.get_dist(this.x, this.y, tx, ty);
let angle = Math.atan2(ty - this.y, tx - this.x); // 计算角度 | use arctan function compute angle
this.vx = Math.cos(angle); // compute vx and vy
this.vy = Math.sin(angle);
}
// 玩家被击中
is_attacked(angle, damage) {
// 被击中之后的烟花粒子效果
for (let i = 0; i < 20 + Math.random() * 10; i ++ ) {
let x = this.x, y = this.y;
let radius = this.radius * Math.random() * 0.1;
let angle = Math.PI * 2 * Math.random();
let vx = Math.cos(angle), vy = Math.sin(angle);
let color = this.color;
let speed = this.speed * 10;
let move_length = this.radius * Math.random() * 5;
new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length);
}
this.radius -= damage;
if (this.radius < 10) {
this.destroy();
return false;
}
this.damage_x = Math.cos(angle);
this.damage_y = Math.sin(angle);
this.damage_speed = damage * 100;
this.speed *= 0.8; // 减慢被攻击的玩家的速度 | slow down attacked player speed
}
update() {
this.spent_time += this.timedelta / 1000;
// 不是自己 && 5s 钟之后再进行攻击 && 朝着玩家每 5s 发射一枚炮弹 | shoot a fireball for player(not ai player) in every 3 second
if (!this.is_me && this.spent_time > 4 && Math.random() < 1 / 300.0) {
let player = this.playground.players[Math.floor(Math.random() * this.playground.players.length)];
let tx = player.x + player.speed * this.vx * this.timedelta / 1000 * 0.3; // ai 预判玩家下一帧的位置,增加游戏难度
let ty = player.y + player.speed * this.vy * this.timedelta / 1000 * 0.3;
this.shoot_fireball(player.x, player.y);
}
if (this.damage_speed > 10) {
this.vx = this.vy = 0;
this.move_length = 0;
this.x += this.damage_x * this.damage_speed * this.timedelta / 1000;
this.y += this.damage_y * this.damage_speed * this.timedelta / 1000;
this.damage_speed *= this.friction;
} else {
if (this.move_length < this.eps) { // 如果已经移动到了指定位置就停止移动| if moved to the specific pos, it stops moving.
this.move_length = 0;
this.vx = this.vy = 0;
if (!this.is_me) { // 如果不是自己,那么就是敌人,每次随机生成一个目标点,向目标点移动
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
} else {
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); // 计算 timedelta 时间间隔内需要真实移动的距离(最后和 move_length 取最小值,防止移动出界) | compute real moving distance in timedelta time intervals.
this.x += this.vx * moved;
this.y += this.vy * moved;
this.move_length -= moved;
}
}
this.render();
}
render() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
on_destroy() { // 当玩家被打死后从数组钟删掉
for (let i = 0; i < this.playground.players.length; i ++ ) {
if (this.playground.players[i] === this) {
this.playground.players.splice(i, 1);
}
}
}
}
playground/skill/fireball/zbase.js
class FireBall extends AcGameObject {
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
super();
this.playground = playground;
this.player = player;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.color = color;
this.speed = speed;
this.move_length = move_length;
this.damage = damage;
this.eps = 0.1;
}
start() {
}
update() {
if (this.move_length < this.eps) { // 火球移动结束,销毁火球 | fireball move end and destroy it
this.destroy();
return false;
}
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * moved;
this.y += this.vy * moved;
this.move_length -= moved;
for (let i = 0; i < this.playground.players.length; i ++ ) {
let player = this.playground.players[i];
if (this.player !== player && this.is_collision(player)) { // 当前玩家不是自己且与其他玩家发生碰撞
this.attack(player);
}
}
this.render();
}
get_dist(x1, y1, x2, y2) {
let dx = x1 - x2;
let dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
// 判断两名玩家是否发生碰撞 - 判断两个中心点之间距离是否小于两个圆的半径之和
is_collision(player) {
let distance = this.get_dist(this.x, this.y, player.x, player.y);
if (distance < this.radius + player.radius)
return true;
return false;
}
// 碰撞后的效果
attack(player) {
let angle = Math.atan2(player.y - this.y, player.x - this.x);
player.is_attacked(angle, this.damage);
this.destroy();
}
render() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
}
playground/particle/zbase.js
class Particle extends AcGameObject { // 粒子效果 | particle effect
constructor(playground, x, y, radius, vx, vy, color, speed, move_length) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.x = x;
this.y = y;
this.radius = radius;
this.vx = vx;
this.vy = vy;
this.color = color;
this.speed = speed;
this.move_length = move_length;
this.friction = 0.9;
this.eps = 1;
}
start() {
}
update() {
if (this.move_length < this.eps || this.speed < this.eps) {
this.destroy();
return false;
}
let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * moved;
this.y += this.vy * moved;
this.speed *= this.friction; // speed slow down
this.move_length -= moved;
this.render();
}
render() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
}