Web项目---拳皇(下)

本文章借鉴https://www.lzhiscoding.xyz

项目实战–拳皇

实现攻击效果

在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
// render方法,表示玩家角色在游戏画面中的呈现形式,这里不是简单地使用矩形填充绘制,而是根据玩家角色当前的状态,从对应的动画信息中获取相应帧的图像,并绘制到游戏画面上,实现更丰富的动画效果展示
render() {

this.ctx.fillStyle = 'blue';
this.ctx.fillRect(this.x, this.y, this.width, this.height);

//如果是正方向
if (this.direction > 0) {
//画一个矩形表示挥拳的范围,以便于做碰撞检测
this.ctx.fillStyle = 'red';
this.ctx.fillRect(this.x + 120, this.y + 40, 100, 20);
} else {
this.ctx.fillStyle = 'red';
this.ctx.fillRect(this.x + this.width - 120 - 100, this.y + 40, 100, 20);
}

let status = this.status;

// 这里添加了一个逻辑判断,当角色当前处于向前移动状态(status === 1),并且角色的方向(direction)与水平速度(vx)的乘积小于 0,
// 意味着角色实际在朝与当前设定方向相反的方向移动(比如角色面朝右,但正在向左移动),此时将状态调整为向后移动状态(status = 2),
// 是为了切换到对应的向后移动动画来展示更符合实际视觉效果的动画表现
if (this.status === 1 && this.direction * this.vx < 0) {
status = 2;
}

let obj = this.animations.get(status);
if (obj && obj.loaded) {
if (this.direction > 0) {
// 根据当前帧数(frame_current_cnt)和当前状态动画的帧率(frame_rate)计算出当前应该显示的动画帧索引k,通过取余操作确保帧索引在有效范围内(不超过总帧数)
let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;
let image = obj.gif.frames[k].image;
// 使用绘图上下文对象(ctx)的drawImage方法,将计算出的当前帧图像绘制到游戏画面上,同时根据动画的缩放比例(scale)和垂直方向的偏移量(offset_y)调整图像的大小和位置,使其正确显示在玩家角色对应的坐标位置上
this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);
} else {
// 如果角色方向为负(向左),则需要翻转图像
this.ctx.save();
this.ctx.scale(-1, 1); // 水平翻转画布
this.ctx.translate(-this.root.game_map.$canvas.width(), 0); // 调整画布位置

let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;
let image = obj.gif.frames[k].image;

// 绘制翻转后的图像
this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);

this.ctx.restore(); // 恢复画布状态
}
}
// 当角色处于攻击状态(status === 4)时,判断当前帧数是否达到该攻击状态动画最后一帧(通过比较当前帧数与帧率乘以总帧数减 1 的值),若达到,说明攻击动画播放完毕,将角色状态设为空闲状态(0)
if (status === 4) {
if (this.frame_current_cnt == obj.frame_rate * (obj.frame_cnt - 1)) {
this.status = 0;
}
}

this.frame_current_cnt++;
// 每渲染一次,当前帧数自增1,用于控制动画的逐帧播放,使得下一帧渲染时能切换到下一张动画帧(根据帧率等因素决定)
}

实现攻击函数、碰撞检测函数和被攻击函数,另外还有render函数需要特判一下被攻击的状态

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import { MyGameObject } from '/static/js/MyGameObject/base.js';

export class Player extends MyGameObject {
// 类的构造函数,在创建Player类的实例时会被调用,接收两个参数:root(即MyGameObject对象)和info(玩家的信息)
constructor(root, info) {
super(); // 调用父类的构造函数
this.root = root; // 将传入的root对象赋值给当前实例的root属性
this.id = info.id; // 从info对象中获取玩家角色的id信息,并赋值给当前实例的id属性
this.x = info.x; // 从info对象中获取玩家角色的初始x坐标信息,并赋值给当前实例的x属性
this.y = info.y; // 从info对象中获取玩家角色的初始y坐标信息,并赋值给当前实例的y属性
this.width = info.width; // 获取玩家角色的宽度信息,赋值给当前实例的width属性
this.height = info.height; // 获取玩家角色的高度信息,赋值给当前实例的height属性
this.color = info.color; // 获取玩家角色的颜色信息,赋值给当前实例的color属性
this.vx = 0; // 初始化玩家角色在水平方向的速度分量为0
this.vy = 0; // 初始化玩家角色在垂直方向的速度分量为0
this.speedx = 400; // 定义玩家角色在水平方向的移动速度
this.speedy = -1000; // 定义玩家角色在跳跃时的初始垂直速度
this.ctx = this.root.game_map.ctx; // 获取游戏地图的绘图上下文对象
this.gravity = 50; // 定义玩家角色所受的重力加速度
this.direction = 1; // 定义玩家角色的方向,正方向为1
this.status = 3; // 初始值为3,表示玩家角色在游戏开始时是跳跃状态
this.pressed_Keys = this.root.game_map.controller.pressed_Keys; // 获取按键状态
this.animations = new Map(); // 用来存放玩家每一个状态的动画
this.frame_current_cnt = 0; // 表示当前帧数
}

start() {
// 初始化方法,可以在子类中重写以执行一些初始化操作
}

// 定义move方法,用于处理玩家角色的移动逻辑
move() {
if (this.status === 3) {
this.vy += this.gravity; // 在垂直方向上,根据重力加速度来更新垂直速度分量
}
this.x += this.vx * this.timedelta / 1000; // 更新玩家角色的水平坐标
this.y += this.vy * this.timedelta / 1000; // 更新玩家角色的垂直坐标

if (this.y > 450) { // 判断玩家角色是否到达地面
this.y = 450; // 将玩家角色的垂直坐标设置为450
this.vy = 0; // 将垂直速度分量重置为0
this.status = 0; // 将玩家角色的状态设置为空闲状态
}
if (this.x < 0) { // 限制玩家角色在水平方向上的移动范围,避免超出画布左边界
this.x = 0;
} else if (this.x + this.width > this.root.game_map.$canvas.width()) { // 限制玩家角色在水平方向上的移动范围,避免超出画布右边界
this.x = 1280 - this.width;
}
}

// update_control 方法,用于根据按键状态更新玩家角色的控制逻辑
update_control() {
let w, a, d, space;
if (this.id === 0) { // 根据玩家角色的 id 确定按键映射
w = this.pressed_Keys.has('w'); // 玩家 1 的跳跃键
a = this.pressed_Keys.has('a'); // 玩家 1 的向左键
d = this.pressed_Keys.has('d'); // 玩家 1 的向右键
space = this.pressed_Keys.has(' '); // 玩家 1 的攻击键
} else {
w = this.pressed_Keys.has('ArrowUp'); // 玩家 2 的跳跃键
a = this.pressed_Keys.has('ArrowLeft'); // 玩家 2 的向左键
d = this.pressed_Keys.has('ArrowRight'); // 玩家 2 的向右键
space = this.pressed_Keys.has('Enter'); // 玩家 2 的攻击键
}

if (this.status === 0 || this.status === 1) { // 若玩家角色处于空闲或移动状态
if (space) { // 如果按下攻击键
this.status = 4; // 将角色状态设为攻击状态
this.vx = 0; // 将水平速度设为0
this.frame_current_cnt = 0; // 重置当前帧数为0
} else if (w) { // 如果按下跳跃键
if (d) { // 同时按下向右键
this.vx = this.speedx; // 设置水平速度为正方向
} else if (a) { // 同时按下向左键
this.vx = -this.speedx; // 设置水平速度为负方向
} else {
this.vx = 0; // 否则水平速度为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; // 水平速度为0
this.status = 0; // 状态设置为空闲状态
}
}
}

// update_direction 方法,用于更新玩家角色的方向
update_direction() {
let players = this.root.players;
if (players[0] && players[1]) { // 如果存在两个玩家
let me = this, you = players[1 - this.id]; // 获取当前玩家和对手玩家
if (me.x < you.x) me.direction = 1; // 如果当前玩家在左侧,则方向为正(向右)
else me.direction = -1; // 否则方向为负(向左)
}
}

// is_attack 方法,用于设置玩家角色为被攻击状态
is_attack() {
this.status = 5; // 将状态设置为被攻击状态
this.frame_current_cnt = 0; // 重置当前帧数为0
}

// is_collision 方法,用于检测两个矩形是否发生碰撞
is_collision(r1, r2) {
if (Math.max(r1.x1, r2.x1) > Math.min(r1.x2, r2.x2)) return false; // 判断x轴是否有交集
if (Math.max(r1.y1, r2.y1) > Math.min(r1.y2, r2.y2)) return false; // 判断y轴是否有交集
return true; // 如果x轴和y轴都有交集,则发生碰撞
}

// update_attack 方法,用于处理玩家角色的攻击逻辑
update_attack() {
if (this.status === 4 && this.frame_current_cnt === 18) { // 如果玩家角色处于攻击状态且当前帧数为18
let me = this, you = this.root.players[1 - this.id]; // 获取当前玩家和对手玩家
let r1;
if (this.direction > 0) { // 如果玩家角色方向为正(向右)
r1 = {
x1: me.x + 120,
y1: me.y + 40,
x2: me.x + 120 + 100,
y2: me.y + 40 + 20,
};
} else { // 如果玩家角色方向为负(向左)
r1 = {
x1: me.x + me.width - 120 - 100,
y1: me.y + 40,
x2: me.x + me.width - 120 - 100 + 100,
y2: me.y + 40 + 20,
};
}
let r2 = { // 对手玩家的矩形区域
x1: you.x,
y1: you.y,
x2: you.x + you.width,
y2: you.y + you.height,
};

if (this.is_collision(r1, r2)) { // 如果两个矩形发生碰撞
you.is_attack(); // 设置对手玩家为被攻击状态
}
}
}

// update 方法,用于更新玩家角色的状态
update() {
this.update_control(); // 更新控制逻辑
this.move(); // 更新移动逻辑
this.update_direction(); // 更新方向
this.update_attack(); // 更新攻击逻辑
this.render(); // 渲染玩家角色
}

// render 方法,用于渲染玩家角色
render() {
let status = this.status;

if (this.status === 1 && this.direction * this.vx < 0) { // 如果玩家角色处于移动状态且方向与速度方向相反
status = 2; // 将状态设置为向后移动状态
}

let obj = this.animations.get(status); // 获取当前状态的动画对象
if (obj && obj.loaded) { // 如果动画对象存在且已加载
if (this.direction > 0) { // 如果玩家角色方向为正(向右)
let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt; // 计算当前帧索引
let image = obj.gif.frames[k].image; // 获取当前帧的图像
this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale); // 绘制图像
} else { // 如果玩家角色方向为负(向左)
this.ctx.save(); // 保存画布状态
this.ctx.scale(-1, 1); // 水平翻转画布
this.ctx.translate(-this.root.game_map.$canvas.width(), 0); // 调整画布位置

let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt; // 计算当前帧索引
let image = obj.gif.frames[k].image; // 获取当前帧的图像

this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale); // 绘制翻转后的图像

this.ctx.restore(); // 恢复画布状态
}
}

if (status === 4 || status === 5) { // 如果玩家角色处于攻击或被攻击状态
if (this.frame_current_cnt == obj.frame_rate * (obj.frame_cnt - 1)) { // 如果当前帧数为动画的最后一帧
this.status = 0; // 将状态设置为空闲状态
}
}

this.frame_current_cnt++; // 当前帧数自增1
}
}

添加生命值,死亡时更换倒地动画,并且render函数特判倒地状态,另外倒地后人物朝向不变和倒地后无法被再次攻击

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import { MyGameObject } from '/static/js/MyGameObject/base.js';

export class Player extends MyGameObject {
// 类的构造函数,在创建Player类的实例时会被调用,接收两个参数:root(即MyGameObject对象)和info(玩家的信息)
constructor(root, info) {
super(); // 调用父类的构造函数
this.root = root; // 将传入的root对象赋值给当前实例的root属性
this.id = info.id; // 从info对象中获取玩家角色的id信息,并赋值给当前实例的id属性
this.x = info.x; // 从info对象中获取玩家角色的初始x坐标信息,并赋值给当前实例的x属性
this.y = info.y; // 从info对象中获取玩家角色的初始y坐标信息,并赋值给当前实例的y属性
this.width = info.width; // 获取玩家角色的宽度信息,赋值给当前实例的width属性
this.height = info.height; // 获取玩家角色的高度信息,赋值给当前实例的height属性
this.color = info.color; // 获取玩家角色的颜色信息,赋值给当前实例的color属性
this.vx = 0; // 初始化玩家角色在水平方向的速度分量为0
this.vy = 0; // 初始化玩家角色在垂直方向的速度分量为0
this.speedx = 400; // 定义玩家角色在水平方向的移动速度
this.speedy = -1000; // 定义玩家角色在跳跃时的初始垂直速度
this.ctx = this.root.game_map.ctx; // 获取游戏地图的绘图上下文对象
this.gravity = 50; // 定义玩家角色所受的重力加速度
this.direction = 1; // 定义玩家角色的方向,正方向为1
this.status = 3; // 初始值为3,表示玩家角色在游戏开始时是跳跃状态
this.pressed_Keys = this.root.game_map.controller.pressed_Keys; // 获取按键状态
this.animations = new Map(); // 用来存放玩家每一个状态的动画
this.frame_current_cnt = 0; // 表示当前帧数
this.hp = 100; // 初始化玩家角色的生命值为100
}

start() {
// 初始化方法,可以在子类中重写以执行一些初始化操作
}

// 定义move方法,用于处理玩家角色的移动逻辑
move() {
if (this.status === 3) {
this.vy += this.gravity; // 在垂直方向上,根据重力加速度来更新垂直速度分量
}
this.x += this.vx * this.timedelta / 1000; // 更新玩家角色的水平坐标
this.y += this.vy * this.timedelta / 1000; // 更新玩家角色的垂直坐标

if (this.y > 450) { // 判断玩家角色是否到达地面
this.y = 450; // 将玩家角色的垂直坐标设置为450
this.vy = 0; // 将垂直速度分量重置为0
this.status = 0; // 将玩家角色的状态设置为空闲状态
}
if (this.x < 0) { // 限制玩家角色在水平方向上的移动范围,避免超出画布左边界
this.x = 0;
} else if (this.x + this.width > this.root.game_map.$canvas.width()) { // 限制玩家角色在水平方向上的移动范围,避免超出画布右边界
this.x = 1280 - this.width;
}
}

// update_control 方法,用于根据按键状态更新玩家角色的控制逻辑
update_control() {
let w, a, d, space;
if (this.id === 0) { // 根据玩家角色的 id 确定按键映射
w = this.pressed_Keys.has('w'); // 玩家 1 的跳跃键
a = this.pressed_Keys.has('a'); // 玩家 1 的向左键
d = this.pressed_Keys.has('d'); // 玩家 1 的向右键
space = this.pressed_Keys.has(' '); // 玩家 1 的攻击键
} else {
w = this.pressed_Keys.has('ArrowUp'); // 玩家 2 的跳跃键
a = this.pressed_Keys.has('ArrowLeft'); // 玩家 2 的向左键
d = this.pressed_Keys.has('ArrowRight'); // 玩家 2 的向右键
space = this.pressed_Keys.has('Enter'); // 玩家 2 的攻击键
}

if (this.status === 0 || this.status === 1) { // 若玩家角色处于空闲或移动状态
if (space) { // 如果按下攻击键
this.status = 4; // 将角色状态设为攻击状态
this.vx = 0; // 将水平速度设为0
this.frame_current_cnt = 0; // 重置当前帧数为0
} else if (w) { // 如果按下跳跃键
if (d) { // 同时按下向右键
this.vx = this.speedx; // 设置水平速度为正方向
} else if (a) { // 同时按下向左键
this.vx = -this.speedx; // 设置水平速度为负方向
} else {
this.vx = 0; // 否则水平速度为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; // 水平速度为0
this.status = 0; // 状态设置为空闲状态
}
}
}

// update_direction 方法,用于更新玩家角色的方向
update_direction() {
if (this.status === 6) return; // 如果玩家角色处于死亡状态,则不更新方向
let players = this.root.players;
if (players[0] && players[1]) { // 如果存在两个玩家
let me = this, you = players[1 - this.id]; // 获取当前玩家和对手玩家
if (me.x < you.x) me.direction = 1; // 如果当前玩家在左侧,则方向为正(向右)
else me.direction = -1; // 否则方向为负(向左)
}
}

// is_attack 方法,用于设置玩家角色为被攻击状态
is_attack() {
if (this.status === 6) return; // 如果玩家角色处于死亡状态,则不再处理被攻击逻辑

this.status = 5; // 将状态设置为被攻击状态
this.frame_current_cnt = 0; // 重置当前帧数为0
this.hp = Math.max(0, this.hp - 10); // 减少玩家角色的生命值,最低为0
if (this.hp <= 0) { // 如果生命值小于等于0
this.status = 6; // 将状态设置为死亡状态
this.frame_current_cnt = 0; // 重置当前帧数为0
}
}

// is_collision 方法,用于检测两个矩形是否发生碰撞
is_collision(r1, r2) {
if (Math.max(r1.x1, r2.x1) > Math.min(r1.x2, r2.x2)) return false; // 判断x轴是否有交集
if (Math.max(r1.y1, r2.y1) > Math.min(r1.y2, r2.y2)) return false; // 判断y轴是否有交集
return true; // 如果x轴和y轴都有交集,则发生碰撞
}

// update_attack 方法,用于处理玩家角色的攻击逻辑
update_attack() {
if (this.status === 4 && this.frame_current_cnt === 18) { // 如果玩家角色处于攻击状态且当前帧数为18
let me = this, you = this.root.players[1 - this.id]; // 获取当前玩家和对手玩家
let r1;
if (this.direction > 0) { // 如果玩家角色方向为正(向右)
r1 = {
x1: me.x + 120,
y1: me.y + 40,
x2: me.x + 120 + 100,
y2: me.y + 40 + 20,
};
} else { // 如果玩家角色方向为负(向左)
r1 = {
x1: me.x + me.width - 120 - 100,
y1: me.y + 40,
x2: me.x + me.width - 120 - 100 + 100,
y2: me.y + 40 + 20,
};
}
let r2 = { // 对手玩家的矩形区域
x1: you.x,
y1: you.y,
x2: you.x + you.width,
y2: you.y + you.height,
};

if (this.is_collision(r1, r2)) { // 如果两个矩形发生碰撞
you.is_attack(); // 设置对手玩家为被攻击状态
}
}
}

// update 方法,用于更新玩家角色的状态
update() {
this.update_control(); // 更新控制逻辑
this.move(); // 更新移动逻辑
this.update_direction(); // 更新方向
this.update_attack(); // 更新攻击逻辑
this.render(); // 渲染玩家角色
}

// render 方法,用于渲染玩家角色
render() {
let status = this.status;

if (this.status === 1 && this.direction * this.vx < 0) { // 如果玩家角色处于移动状态且方向与速度方向相反
status = 2; // 将状态设置为向后移动状态
}

let obj = this.animations.get(status); // 获取当前状态的动画对象
if (obj && obj.loaded) { // 如果动画对象存在且已加载
if (this.direction > 0) { // 如果玩家角色方向为正(向右)
let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt; // 计算当前帧索引
let image = obj.gif.frames[k].image; // 获取当前帧的图像
this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale); // 绘制图像
} else { // 如果玩家角色方向为负(向左)
this.ctx.save(); // 保存画布状态
this.ctx.scale(-1, 1); // 水平翻转画布
this.ctx.translate(-this.root.game_map.$canvas.width(), 0); // 调整画布位置

let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt; // 计算当前帧索引
let image = obj.gif.frames[k].image; // 获取当前帧的图像

this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale); // 绘制翻转后的图像

this.ctx.restore(); // 恢复画布状态
}
}

if (status === 4 || status === 5 || status === 6) { // 如果玩家角色处于攻击、被攻击或死亡状态
if (this.frame_current_cnt == obj.frame_rate * (obj.frame_cnt - 1)) { // 如果当前帧数为动画的最后一帧
if (status === 6) {
this.frame_current_cnt--; // 如果玩家角色处于死亡状态,则保持最后一帧
} else {
this.status = 0; // 将状态设置为空闲状态
}
}
}

this.frame_current_cnt++; // 当前帧数自增1
}
}

修改所有状态都会受到重力,特判只有在跳跃状态落地后改为静止状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义move方法,用于处理玩家角色的移动逻辑
move() {
this.vy += this.gravity; // 在垂直方向上,根据重力加速度来更新垂直速度分量

this.x += this.vx * this.timedelta / 1000; // 更新玩家角色的水平坐标
this.y += this.vy * this.timedelta / 1000; // 更新玩家角色的垂直坐标

if (this.y > 450) { // 判断玩家角色是否到达地面
this.y = 450; // 将玩家角色的垂直坐标设置为450
this.vy = 0; // 将垂直速度分量重置为0
if (this.status === 3)
this.status = 0; // 如果玩家角色之前处于跳跃状态(status === 3),则将其状态设置为空闲状态(status === 0)
}
if (this.x < 0) { // 限制玩家角色在水平方向上的移动范围,避免超出画布左边界
this.x = 0;
} else if (this.x + this.width > this.root.game_map.$canvas.width()) { // 限制玩家角色在水平方向上的移动范围,避免超出画布右边界
this.x = 1280 - this.width;
}
}

实现血条功能和计时器

固定画面在html里写,减少canvas渲染的内容,用js控制html,写在GameMap类

在GameMap类的构造函数的末尾添加以下代码:

1
2
3
4
5
6
7
8
// 在 $kof 元素中添加一个头部区域,用于显示游戏信息(如玩家血量、计时器等)
this.root.$kof.append($(`
<div class="kof-head">
<div class="kof-head-hp-0"></div>
<div class="kof-head-timer">60</div>
<div class="kof-head-hp-1"></div>
</div>
`));

设置血条定时器的样式

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
#kof {
width: 1280px;
/* 设置容器的宽度为 1280 像素 */
height: 720px;
/* 设置容器的高度为 720 像素 */

/* 设置背景图片,使用 URL 指定图片路径 */
background-image: url('/static/images/background/background.gif');
/* 设置背景图片的尺寸为 200% 宽度和 100% 高度,使背景图片横向拉伸 */
background-size: 200% 100%;
/* 设置背景图片的位置为顶部对齐 */
background-position: top;
/* 设置容器的定位方式为绝对定位 */
position: absolute;
}

#kof>.kof-head {
width: 100%;
/* 设置头部区域的宽度为 100%,与容器宽度一致 */
height: 80px;
/* 设置头部区域的高度为 80 像素 */
position: absolute;
/* 设置头部区域的定位方式为绝对定位 */
top: 0;
/* 将头部区域定位在容器的顶部 */
display: flex;
/* 使用 Flex 布局,使子元素水平排列 */
align-items: center;
/* 将子元素垂直居中 */
}

#kof>.kof-head>.kof-head-hp-0 {
height: 40px;
/* 设置玩家 1 血量条的高度为 40 像素 */
width: calc(50% - 60px);
/* 设置玩家 1 血量条的宽度为容器宽度的一半减去 60 像素 */
margin-left: 20px;
/* 设置左边距为 20 像素 */
border: white 5px solid;
/* 设置边框为白色,宽度为 5 像素 */
border-right: none;
/* 去掉右侧边框 */
box-sizing: border-box;
/* 设置盒模型为 border-box,使边框宽度包含在元素宽度内 */
}

#kof>.kof-head>.kof-head-timer {
height: 60px;
/* 设置计时器的高度为 60 像素 */
width: 80px;
/* 设置计时器的宽度为 80 像素 */
background-color: orange;
/* 设置背景颜色为橙色 */
border: white 5px solid;
/* 设置边框为白色,宽度为 5 像素 */
box-sizing: border-box;
/* 设置盒模型为 border-box,使边框宽度包含在元素宽度内 */
color: white;
/* 设置文字颜色为白色 */
font-size: 30px;
/* 设置字体大小为 30 像素 */
font-weight: 800;
/* 设置字体粗细为 800(加粗) */
text-align: center;
/* 设置文字水平居中 */
line-height: 50px;
/* 设置行高为 50 像素,使文字垂直居中 */
user-select: none;
/* 禁止用户选择文字 */
}

#kof>.kof-head>.kof-head-hp-1 {
height: 40px;
/* 设置玩家 2 血量条的高度为 40 像素 */
width: calc(50% - 60px);
/* 设置玩家 2 血量条的宽度为容器宽度的一半减去 60 像素 */
border: white 5px solid;
/* 设置边框为白色,宽度为 5 像素 */
border-left: none;
/* 去掉左侧边框 */
box-sizing: border-box;
/* 设置盒模型为 border-box,使边框宽度包含在元素宽度内 */
}

实现血条按照百分比缩减

先把布局和样式写好
GameMap类

1
2
3
4
5
6
7
8
9
10
11
12
// 在 $kof 元素中添加一个头部区域,用于显示游戏信息(如玩家血量、计时器等)
this.root.$kof.append($(`
<div class="kof-head">
<div class="kof-head-hp-0">
<div><div></div></div>
</div>
<div class="kof-head-timer">60</div>
<div class="kof-head-hp-1">
<div><div></div></div>
</div>
</div>
`));

css文件

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
#kof {
width: 1280px; /* 设置容器的宽度为 1280 像素 */
height: 720px; /* 设置容器的高度为 720 像素 */

/* 设置背景图片,使用 URL 指定图片路径 */
background-image: url('/static/images/background/background.gif');
/* 设置背景图片的尺寸为 200% 宽度和 100% 高度,使背景图片横向拉伸 */
background-size: 200% 100%;
/* 设置背景图片的位置为顶部对齐 */
background-position: top;
/* 设置容器的定位方式为绝对定位 */
position: absolute;
}

#kof>.kof-head {
width: 100%; /* 设置头部区域的宽度为 100%,与容器宽度一致 */
height: 80px; /* 设置头部区域的高度为 80 像素 */
position: absolute; /* 设置头部区域的定位方式为绝对定位 */
top: 0; /* 将头部区域定位在容器的顶部 */
display: flex; /* 使用 Flex 布局,使子元素水平排列 */
align-items: center; /* 将子元素垂直居中 */
}

#kof>.kof-head>.kof-head-hp-0 {
height: 40px; /* 设置玩家 1 血量条的高度为 40 像素 */
width: calc(50% - 60px); /* 设置玩家 1 血量条的宽度为容器宽度的一半减去 60 像素 */
margin-left: 20px; /* 设置左边距为 20 像素 */
border: white 5px solid; /* 设置边框为白色,宽度为 5 像素 */
border-right: none; /* 去掉右侧边框 */
box-sizing: border-box; /* 设置盒模型为 border-box,使边框宽度包含在元素宽度内 */
}

#kof>.kof-head>.kof-head-timer {
height: 60px; /* 设置计时器的高度为 60 像素 */
width: 80px; /* 设置计时器的宽度为 80 像素 */
background-color: orange; /* 设置背景颜色为橙色 */
border: white 5px solid; /* 设置边框为白色,宽度为 5 像素 */
box-sizing: border-box; /* 设置盒模型为 border-box,使边框宽度包含在元素宽度内 */
color: white; /* 设置文字颜色为白色 */
font-size: 30px; /* 设置字体大小为 30 像素 */
font-weight: 800; /* 设置字体粗细为 800(加粗) */
text-align: center; /* 设置文字水平居中 */
line-height: 50px; /* 设置行高为 50 像素,使文字垂直居中 */
user-select: none; /* 禁止用户选择文字 */
}

#kof>.kof-head>.kof-head-hp-1 {
height: 40px; /* 设置玩家 2 血量条的高度为 40 像素 */
width: calc(50% - 60px); /* 设置玩家 2 血量条的宽度为容器宽度的一半减去 60 像素 */
border: white 5px solid; /* 设置边框为白色,宽度为 5 像素 */
border-left: none; /* 去掉左侧边框 */
box-sizing: border-box; /* 设置盒模型为 border-box,使边框宽度包含在元素宽度内 */
}

#kof>.kof-head>.kof-head-hp-0>div {
background-color: red; /* 设置玩家 1 血量条的背景颜色为红色 */
height: 100%; /* 设置高度为 100%,填充父容器 */
width: 100%; /* 设置宽度为 100%,填充父容器 */
float: right; /* 使用浮动将内容靠右对齐 */
}

#kof>.kof-head>.kof-head-hp-1>div {
background-color: red; /* 设置玩家 2 血量条的背景颜色为红色 */
height: 100%; /* 设置高度为 100%,填充父容器 */
width: 100%; /* 设置宽度为 100%,填充父容器 */
}

#kof>.kof-head>.kof-head-hp-0>div>div {
background-color: lightgreen; /* 设置玩家 1 血量条的当前血量背景颜色为浅绿色 */
height: 100%; /* 设置高度为 100%,填充父容器 */
width: 100%; /* 设置宽度为 100%,填充父容器 */
float: right; /* 使用浮动将内容靠右对齐 */
}

#kof>.kof-head>.kof-head-hp-1>div>div {
background-color: lightgreen; /* 设置玩家 2 血量条的当前血量背景颜色为浅绿色 */
height: 100%; /* 设置高度为 100%,填充父容器 */
width: 100%; /* 设置宽度为 100%,填充父容器 */
}

再实现js逻辑,Player类中

在构造函数最后

1
2
this.$hp = this.root.$kof.find(`.kof-head-hp-${this.id}>div`); // 获取玩家血量条的jQuery对象
this.$hp_div = this.$hp.find('div'); // 获取玩家血量条中表示当前血量的jQuery对象

在is_attack函数实现逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// is_attack 方法,用于设置玩家角色为被攻击状态
is_attack() {
if (this.status === 6) return; // 如果玩家角色处于死亡状态,则不再处理被攻击逻辑

this.status = 5; // 将状态设置为被攻击状态
this.frame_current_cnt = 0; // 重置当前帧数为0
this.hp = Math.max(0, this.hp - 10); // 减少玩家角色的生命值,最低为0

// 更新血量条的动画效果
this.$hp_div.animate({
width: this.$hp.parent().width() * this.hp / 100 // 设置当前血量的宽度
}, 300); // 动画持续时间为300毫秒
this.$hp.animate({
width: this.$hp.parent().width() * this.hp / 100 // 设置总血量的宽度
}, 600); // 动画持续时间为600毫秒

if (this.hp <= 0) { // 如果生命值小于等于0
this.status = 6; // 将状态设置为死亡状态
this.frame_current_cnt = 0; // 重置当前帧数为0
}
}

实现定时器功能

在GameMap类构造函数定义时间

1
2
3
4
// 初始化剩余时间,单位为毫秒
this.time_left = 60000;
// 获取计时器元素的引用,方便后续更新显示
this.$timer = this.root.$kof.find(".kof-head-timer");

在update函数里更新时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// update方法,每一帧都会被调用,用于更新游戏状态
update() {
// 减少剩余时间
this.time_left -= this.timedelta;
// 如果时间小于0,重置为0
if (this.time_left < 0) {
this.time_left = 0;
// 获取两个玩家对象
let [a, b] = this.root.players;
// 如果两个玩家都不处于状态6(假设状态6为游戏结束状态),则设置他们的状态为6,并重置一些属性
if (a.status !== 6 && b.status !== 6) {
a.status = b.status = 6;
a.frame_current_cnt = b.frame_current_cnt = 0;
a.vx = b.vx = 0;//死亡后速度归零
}
}
// 更新计时器显示
this.$timer.text(parseInt(this.time_left / 1000));

// 调用render方法进行渲染
this.render();
}

在Player类的is_attack函数里将死亡后的速度归零

1
2
3
4
5
if (this.hp <= 0) { // 如果生命值小于等于0
this.status = 6; // 将状态设置为死亡状态
this.frame_current_cnt = 0; // 重置当前帧数为0
this.vx = 0;//死亡后速度为0
}