Django学习笔记-创建游戏界面

  1. 1. 模块化引入JS变量
  2. 2. 实现物体运动基类
  3. 3. Canvas绘制游戏画面
  4. 4. 创建游戏角色及实现移动效果
  5. 5. 创建角色技能
  6. 6. 创建敌人及实现简单AI
  7. 7. 火球碰撞检测
  8. 8. 实现被攻击时的粒子效果
  9. 9. 敌人随机颜色
  10. 10. 敌人自动攻击

本节内容是游戏界面的设计,包括各个目标的绘制、角色的移动与攻击、AI 敌人的设计等部分。

1. 模块化引入JS变量

首先我们需要对之前的代码进行一点小修改,在 web.html 中使用 <script> 会导致定义的所有 Class(例如 AcGame)都会变成网页的全局变量,当引入多个 JS 文件后网页可能会出现重名变量导致冲突,我们最好做一个模块化,当我们需要某个名称的时候,我们只将这一个名称引入进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{% load static %}

<head>
<link rel="stylesheet" href="http://8.130.54.44:8000/static/css/jquery-ui.min.css">
<script src="http://8.130.54.44:8000/static/js/jquery-3.6.1.min.js"></script>
<link rel="stylesheet" href="{% static 'css/game.css' %}">
</head>

<body style="margin: 0">
<div id="ac_game_1"></div>
<script type="module">
import {AcGame} from "{% static 'js/dist/game.js' %}";
$(document).ready(function() {
let ac_game = new AcGame("ac_game_1");
});
</script>
</body>

这时候我们刷新网页会看到报错:Uncaught SyntaxError: The requested module '/static/js/dist/game.js' does not provide an export named 'AcGame',表示如果我们想加载 AcGame 的话需要在这个类前面加一个关键字 export

1
2
3
4
5
6
7
8
9
10
11
12
13
export class AcGame {
constructor(id) {
this.id = id;
this.$ac_game = $('#' + id); // jQuery通过id找对象的方式
this.menu = new AcGameMenu(this);
this.playground = new AcGamePlayground(this);

this.start();
}

start() {
}
}

此时再次刷新网页即可看到报错消失。

2. 实现物体运动基类

在游戏中物体的运动效果是通过不断刷新界面实现的,浏览器每秒刷新60次(即60帧),每一帧都是一张图片,因此我们需要先实现一个能够每一帧都调用对象的刷新函数的基类(简易游戏引擎)。

我们在 static/js/src/playground/ 目录下创建一个 ac_game_object 目录,然后进入该目录创建 zbase.js

这个类一般有三个函数,函数 start 在开始时执行一次,用于创建对象时初始化对象的颜色、分值、昵称等信息(从服务器端加载出来),函数 update 每一帧都会执行一次,函数 destroy 表示删除当前对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let AC_GAME_OBJECTS = [];  // 将所有对象加到一个全局数组里,之后可以遍历每个对象刷新一次

class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this);
}

start() { // 只会在第一帧执行一次
}

update() { // 每一帧都会执行一次
}

on_destroy() { // 在被删除前执行一次
}

destroy() { // 删掉该对象
this.on_destroy();

for (let i = 0; i < AC_GAME_OBJECTS.length; i++) {
if (AC_GAME_OBJECTS[i] === this) {
AC_GAME_OBJECTS.splice(i, 1); // 从位置i开始删除1个
break;
}
}
}
}

然后我们需要实现每一帧循环渲染一遍全局数组中的对象,JS 提供了一个 API:requestAnimationFrame(),该函数会在一秒钟调用60次,也就是将一秒钟分成60等份,在下一帧时执行一遍函数,我们可以定义一个函数 AC_GAME_ANIMATION 表示每帧需要执行的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
let AC_GAME_OBJECTS = [];  // 将所有对象加到一个全局数组里,之后可以遍历每个对象刷新一次

class AcGameObject {
constructor() {
AC_GAME_OBJECTS.push(this);

this.has_called_start = false; // 是否执行过start函数
this.timedelta = 0; // 当前帧距离上一帧的时间间隔,因为如果用帧来衡量的话不同浏览器可能帧数不一样会导致不同的效果
}

start() { // 只会在第一帧执行一次
}

update() { // 每一帧都会执行一次
}

on_destroy() { // 在被删除前执行一次
}

destroy() { // 删掉该对象
this.on_destroy();

for (let i = 0; i < AC_GAME_OBJECTS.length; i++) {
if (AC_GAME_OBJECTS[i] === this) {
AC_GAME_OBJECTS.splice(i, 1); // 从位置i开始删除1个
break;
}
}
}
}

let last_timestamp; // 上一帧的时间戳

let AC_GAME_ANIMATION = function(timestamp) { // timestamp表示在哪个时刻调用的这个函数
for (let i = 0; i < AC_GAME_OBJECTS.length; i++) {
let obj = AC_GAME_OBJECTS[i];
if (!obj.has_called_start) { // 如果没有执行过start
obj.start();
obj.has_called_start = true;
} else {
obj.timedelta = timestamp - last_timestamp; // 更新一下时间间隔
obj.update();
}
}
last_timestamp = timestamp;

requestAnimationFrame(AC_GAME_ANIMATION) // 递归实现每一帧都调用一次该函数
}

requestAnimationFrame(AC_GAME_ANIMATION);

3. Canvas绘制游戏画面

接下来我们需要创建游戏画面,在 playground 目录下创建一个 game_map 目录,然后创建 zbase.js

1
2
3
4
5
6
7
8
9
10
11
class GameMap extends AcGameObject {
constructor(playground) { // 需要将AcGamePlayground传进来
super(); // 调用基类构造函数,相当于将自己添加到了AC_GAME_OBJECTS中
this.playground = playground;
this.$canvas = $(`<canvas></canvas>`); // 画布,用来渲染画面
this.ctx = this.$canvas[0].getContext('2d'); // 二维画布
this.ctx.canvas.width = this.playground.width; // 设置画布宽度
this.ctx.canvas.height = this.playground.height; // 设置画布高度
this.playground.$playground.append(this.$canvas); // 将画布添加到HTML中
}
}

playground 目录下的 zbase.js 中将游戏画面对象创建出来即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class AcGamePlayground {
constructor(root) {
this.root = root;
this.$playground = $(`
<div class='ac_game_playground'>
</div>
`);
this.root.$ac_game.append(this.$playground);

// 将界面的宽高先存下来
this.width = this.$playground.width();
this.height = this.$playground.height();

this.game_map = new GameMap(this); // 创建游戏画面

this.start();
}

start() {
this.hide(); // 初始化时需要先关闭playground界面
}

// 显示playground界面
show() {
this.$playground.show();
}

// 关闭playground界面
hide() {
this.$playground.hide();
}
}

现在浏览器中的 Canvas 是没有颜色的,我们需要将它渲染出来,即在 GameMap 类中添加一个渲染函数 render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GameMap extends AcGameObject {
constructor(playground) { // 需要将AcGamePlayground传进来
...
}

start() {
}

update() {
this.render(); // 每一帧都要画一次
}

render() {
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; // 黑色背景
// 左上角坐标(0, 0),右下角坐标(w, h)
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
}

4. 创建游戏角色及实现移动效果

playground 目录下创建一个 player 目录,然后创建 zbase.js(角色也是一个游戏对象,因此也要从 AcGameObject 类中扩展出来),角色需要传入中心坐标 x, y、半径 radius、颜色 color、每秒移动的距离百分比 speed、是否为自己 is_me(因为未来在联机的时候自己和敌人的操作方式是不一样的,敌人的操作是通过网络传过来的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1; // 误差小于0.1认为是0
}

start() {
}

update() {
this.render();
}

render() {
this.ctx.beginPath();
// 角度从0画到2PI,是否逆时针为false
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
}

然后我们修改 AcGamePlayground 类将自己创建出来试试效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class AcGamePlayground {
constructor(root) {
this.root = root;
this.$playground = $(`
<div class='ac_game_playground'>
</div>
`);
this.root.$ac_game.append(this.$playground);

// 将界面的宽高先存下来
this.width = this.$playground.width();
this.height = this.$playground.height();

this.game_map = new GameMap(this); // 创建游戏画面

this.players = []; // 所有玩家
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, 'white', this.height * 0.15, true)); // 创建自己

this.start();
}

start() {
this.hide(); // 初始化时需要先关闭playground界面
}

// 显示playground界面
show() {
this.$playground.show();
}

// 关闭playground界面
hide() {
this.$playground.hide();
}
}

接下来我们实现小球的移动,我们需要给每个小球设置一个 X 轴方向的速度和 Y 轴方向的速度,在每次刷新对象的时候更新一下小球的坐标即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
...
this.vx = 0.5; // x轴方向上的速度
this.vy = 0.5; // y轴方向上的速度
}

start() {
}

update() {
this.x += this.vx;
this.y += this.vy;
this.render();
}

render() {
...
}
}

现在我们还需要实现小球移动到鼠标右键点击的位置,如下图所示,假设从 (x, y) 移动到 (tx, ty),移动的距离即为两点间的欧几里得距离,atan2(y, x) 函数可以求出方向角 θ,我们将其移动方向归一化视为一个单位圆,那么 X 轴方向的速度即为 1 * cos(θ),Y 轴方向的速度即为 1 * sin(θ)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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轴方向上的速度
this.vy = 0; // y轴方向上的速度
this.move_length = 0; // 要移动的距离
this.radius = radius;
this.color = color;
this.speed = speed;
this.is_me = is_me;
this.eps = 0.1; // 误差小于0.1认为是0
}

start() {
if (this.is_me) {
this.add_listening_events();
}
}

add_listening_events() {
let outer = this;
this.playground.game_map.$canvas.on('contextmenu', function() {
return false;
}); // 取消右键的菜单功能
this.playground.game_map.$canvas.mousedown(function(e) {
if (e.which === 3) { // 1表示左键,2表示滚轮,3表示右键
outer.move_to(e.clientX, e.clientY); // e.clientX/Y为鼠标点击坐标
}
});
}

// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

move_to(tx, ty) {
this.move_length = this.get_dist(this.x, this.y, tx, ty);
let theta = Math.atan2(ty - this.y, tx - this.x);
this.vx = Math.cos(theta);
this.vy = Math.sin(theta);
}

update() {
if (this.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 0;
} else {
// 计算真实移动距离,与一帧的移动距离取min防止移出界
let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.move_length -= true_move;
}
this.render();
}

render() {
this.ctx.beginPath();
// 角度从0画到2PI,是否逆时针为false
this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
}

5. 创建角色技能

playground 目录下创建一个 skill 目录,我们先实现火球技能,在 skill 目录下再创建 fireball 目录,然后创建 zbase.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class FireBall extends AcGameObject {
// 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length) {
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.eps = 0.1;
}

start() {
}

update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}

let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.move_length -= true_move;

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();
}
}

然后我们要实现玩家选中某个技能后点击鼠标能够使用该技能,获取按键信息时不能用 Canvas,因为不能聚焦,可以用 Window 来获取,每个按键的编号可以在网上查找对照表获得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
...
this.cur_skill = null; // 当前选的技能是什么
}

start() {
if (this.is_me) {
this.add_listening_events();
}
}

add_listening_events() {
let outer = this;
this.playground.game_map.$canvas.on('contextmenu', function() {
return false;
}); // 取消右键的菜单功能
this.playground.game_map.$canvas.mousedown(function(e) {
if (e.which === 3) { // 1表示左键,2表示滚轮,3表示右键
outer.move_to(e.clientX, e.clientY); // e.clientX/Y为鼠标点击坐标
} else if (e.which === 1) {
if (outer.cur_skill === 'fireball') {
outer.shoot_fireball(e.clientX, e.clientY);
}

outer.cur_skill = null; // 释放完一次技能后还原
}
});
$(window).keydown(function(e) {
if (e.which === 81) { // Q键
outer.cur_skill = 'fireball';
return false;
}
});
}

// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

shoot_fireball(tx, ty) {
let x = this.x, y = this.y;
let radius = this.playground.height * 0.01;
let theta = Math.atan2(ty - this.y, tx - this.x);
let vx = Math.cos(theta), vy = Math.sin(theta);
let color = 'orange';
let speed = this.playground.height * 0.5;
let move_length = this.playground.height * 0.8;
new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length);
}

move_to(tx, ty) {
...
}

update() {
...
}

render() {
...
}
}

6. 创建敌人及实现简单AI

首先我们在 AcGamePlayground 类中添加5名敌人:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class AcGamePlayground {
constructor(root) {
...

// 创建敌人
for (let i = 0; i < 5; i++) {
this.players.push(new Player(this, this.width / 2, this.height / 2, this.height * 0.05, 'blue', this.height * 0.15, false));
}

this.start();
}

start() {
this.hide(); // 初始化时需要先关闭playground界面
}

// 显示playground界面
show() {
this.$playground.show();
}

// 关闭playground界面
hide() {
this.$playground.hide();
}
}

然后我们需要让敌人移动起来,可以设定一个随机的目的地,然后移动到该目的地时再随机一个新的目的地,在 Player 中进行相应的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
...
}

start() {
if (this.is_me) {
this.add_listening_events();
} else {
// Math.random()返回一个0~1之间的数
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
}

add_listening_events() {
...
}

// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

shoot_fireball(tx, ty) {
...
}

move_to(tx, ty) {
this.move_length = this.get_dist(this.x, this.y, tx, ty);
let theta = Math.atan2(ty - this.y, tx - this.x);
this.vx = Math.cos(theta);
this.vy = Math.sin(theta);
}

update() {
if (this.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 0;
if (!this.is_me) { // AI敌人不能停下来
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
} else {
// 计算真实移动距离,与一帧的移动距离取min防止移出界
let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.move_length -= true_move;
}
this.render();
}

render() {
...
}
}

7. 火球碰撞检测

两个圆的相交检测很简单,只需要判断两个圆的圆心距离是否小于两圆的半径之和即可,我们在 FireBall 类中进行碰撞检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class FireBall extends AcGameObject {
// 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
...
this.damage = damage; // 伤害
this.eps = 0.1;
}

start() {
}

update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}

let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.move_length -= true_move;

// 碰撞检测
for (let i = 0; i < this.playground.players.length; i++) {
let player = this.playground.players[i];
if (player !== this.player && this.is_collision(player)) {
this.attack(player); // this攻击player
}
}

this.render();
}

get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

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 theta = Math.atan2(player.y - this.y, player.x - this.x);
player.is_attacked(theta, this.damage);
this.destroy();
}

render() {
...
}
}

接着我们需要实现玩家被攻击时的效果,被攻击时有向后的击退效果,且击退过程中玩家无法移动,被攻击后血量减少(使用小球半径表示血量,即被攻击后小球半径变小),血量越少移动速度越快,我们在 Player 类中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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轴方向上的速度
this.vy = 0; // y轴方向上的速度
this.damage_vx = 0; // 被击中后在x轴方向上的速度
this.damage_vy = 0; // 被击中后在y轴方向上的速度
this.damage_speed = 0;
this.move_length = 0; // 要移动的距离
this.radius = radius;
this.color = color;
this.speed = speed; // 每秒移动的距离
this.is_me = is_me;
this.eps = 0.1; // 误差小于0.1认为是0
this.friction = 0.9; // 摩擦力

this.cur_skill = null; // 当前选的技能是什么
}

start() {
...
}

// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

shoot_fireball(tx, ty) {
...
}

move_to(tx, ty) {
...
}

is_attacked(theta, damage) { // 被攻击到
this.radius -= damage;
this.speed *= 1.08; // 血量越少移动越快
if (this.radius < 10) { // 半径小于10像素认为已死
this.destroy();
return false;
}
this.damage_vx = Math.cos(theta);
this.damage_vy = Math.sin(theta);
this.damage_speed = damage * 90;
}

update() {
if (this.damage_speed > this.eps) { // 有击退效果时玩家无法移动
this.vx = this.vy = 0;
this.move_length = 0;
this.x += this.damage_vx * this.damage_speed * this.timedelta / 1000;
this.y += this.damage_vy * this.damage_speed * this.timedelta / 1000;
this.damage_speed *= this.friction;
} else {
if (this.move_length < this.eps) {
this.move_length = 0;
this.vx = this.vy = 0;
if (!this.is_me) { // AI敌人不能停下来
let tx = Math.random() * this.playground.width;
let ty = Math.random() * this.playground.height;
this.move_to(tx, ty);
}
} else {
// 计算真实移动距离,与一帧的移动距离取min防止移出界
let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.move_length -= true_move;
}
}
this.render();
}

render() {
...
}
}

8. 实现被攻击时的粒子效果

我们可以设计一个当被攻击时向前爆出若干小球的粒子效果,在 playground 目录下创建一个 particle 目录,然后创建 zbase.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Particle extends AcGameObject {
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.eps = 1;
this.friction = 0.9;
}

start() {
}

update() {
if (this.move_length < this.eps || this.speed < this.eps) {
this.destroy();
return false;
}
let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.speed *= this.friction;
this.move_length -= true_move;
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();
}
}

然后在 Player 类的被击中函数中创建若干粒子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
...
}

start() {
...
}

add_listening_events() {
...
}

// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

shoot_fireball(tx, ty) {
...
}

move_to(tx, ty) {
...
}

is_attacked(theta, damage) { // 被攻击到
// 创建粒子效果
for (let i = 0; i < 10 + Math.random() * 5; i++) {
let x = this.x, y = this.y;
let radius = this.radius * Math.random() * 0.2;
let theta = Math.PI * 2 * Math.random();
let vx = Math.cos(theta), vy = Math.sin(theta);
let color = this.color;
let speed = this.speed * 10;
let move_length = this.radius * Math.random() * 10;
new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length);
}

this.radius -= damage;
this.speed *= 1.08; // 血量越少移动越快
if (this.radius < 10) { // 半径小于10像素认为已死
this.destroy();
return false;
}
this.damage_vx = Math.cos(theta);
this.damage_vy = Math.sin(theta);
this.damage_speed = damage * 90;
}

update() {
...
}

render() {
...
}
}

9. 敌人随机颜色

最后我们随机生成每个敌人的颜色,在 Playground 类中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AcGamePlayground {
constructor(root) {
...
// 创建敌人
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 colors = ['blue', 'red', 'pink', 'grey', 'green'];
return colors[Math.floor(Math.random() * 5)];
}

start() {
this.hide(); // 初始化时需要先关闭playground界面
}

// 显示playground界面
show() {
this.$playground.show();
}

// 关闭playground界面
hide() {
this.$playground.hide();
}
}

10. 敌人自动攻击

我们可以设置敌人随机向玩家射击,但是开局前几秒限制敌人射击,修改 Player 类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, is_me) {
...
this.spent_time = 0; // 记录游戏时间,刚开局不能攻击

this.cur_skill = null; // 当前选的技能是什么
}

start() {
...
}

add_listening_events() {
...
}

// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

// 向(tx, ty)位置发射火球
shoot_fireball(tx, ty) {
...
}

move_to(tx, ty) {
...
}

is_attacked(theta, damage) { // 被攻击到
...
}

update() {
this.spent_time += this.timedelta / 1000;
// AI敌人随机向玩家射击,游戏刚开始前三秒AI不能射击
if (this.spent_time > 3 && !this.is_me && Math.random() < 1 / 360.0) {
let player = this.playground.players[0];
this.shoot_fireball(player.x, player.y);
}

...
}

render() {
...
}
}

上一章:Django学习笔记-创建菜单界面

下一章:Django学习笔记-部署Nginx与对接AcApp