7、联机对战下
目标:实现剩下的三个同步函数
create player
通过server
同步玩家信息,有几个玩家,各自的状态如何。(上文实现)move to
同步移动,某个玩家的移动需要通知其他玩家,在其他玩家的窗口渲染shoot fireball
火球发射,与同步玩家状态类似,同步火球的状态attack
函数,同步有没有被击中,为了减轻服务器负载、提升玩家体验,在本地判定玩家是否被击中,然后再将结果传给服务器,服务器再将结果更新给其他玩家。
将 player
中原本实现的几个函数广播给其他的窗口即可。
在 acapp
中添加退出功能
利用 AcWingOS
提供的接口,实现点击退出按钮关闭当前小窗口。
在 game/static/js/src/settings/zbase.js
中增加更改 logout_remote
函数
logout_on_remote() {
if (this.platform === "ACAPP") {
this.root.AcWingOS.api.window.close();
}
......
}
move_to
函数的实现
ws的前端
与创建玩家的代码类似,定义两个函数,分别为发送和收到 move_to
的函数
game/static/js/src/playground/socket/multiplayer/zbase.js
// 发送玩家移动信息
send_move_to(tx, ty){
let outer = this;
this.ws.send(JSON.stringify({
'event': "move_to",
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
}));
}
receive_move_to(uuid, tx, ty){
let player = this.get_player(uuid);
if(player) {
player.move_to(tx, ty);
}
}
ws的后端
game/consumers/multiplayer/index.py
async def move_to(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_send_event",
'event': "move_to",
'uuid': data['uuid'],
'tx': data['tx'],
'ty': data['ty'],
}
)
async def group_send_event(self, data):
await self.send(text_data=json.dumps(data))
# 接收前端信息
async def receive(self, text_data):
data = json.loads(text_data)
event = data['event']
if event == "create_player":
await self.create_player(data)
elif event == "move_to":
await self.move_to(data)
调用player
player
类中当识别到鼠标右击时,则确定目标点并进行移动,若当前模式为多人模式 multi mode
则调用 playground
中创建的 websocket
mps
的发送接口。
game/static/js/src/playground/palyer/zbase.js
this.playground.game_map.$canvas.mousedown(function (e) {// 将鼠标右键和该函数绑定
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {// 右键是3,滚轮2,左键是1
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty);
if (outer.playground.mode === "multi mode") {
outer.playground.mps.send_move_to(tx, ty);
}
}
攻击 shoot_fireball
实现
在 Player
类中首先将每个玩家发射的子弹存下来 this.fireballs = [];
在生成一个 fireball
时将它加入到数组中。
火球类析构前将其从对应玩家的数组中删除。
on_destroy() {
let fireballs = this.player.fireballs;
for (let i = 0; i < fireballs.length; i++) {
if (fireballs[i] === this) {
fireballs.splice(i, 1);
break;
}
}
}
mps
中实现 fireball
的 send
和 receive
函数
wss
前端
game/static/js/src/playground/socket/multiplayer/zbase.js
send_shoot_fireball(tx, ty, ball_uuid){
let outer = this;
this.ws.send(JSON.stringify(({
'event': "shoot_fireball",
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
"ball_uuid": ball_uuid,
})));
}
receive_shoot_fireball(uuid, tx, ty, ball_uuid){
let player = this.get_player(uuid);
if (player) {
let fireball = player.shoot_fireball(tx, ty);
fireball.uuid = ball_uuid;
}
}
wss
后端
game/consumers/multiplayer/index.py
async def shoot_fireball(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_send_event",
'event': "shoot_fireball",
'uuid': data["uuid"],
'tx': data['tx'],
'ty': data['ty'],
'ball_uuid': data['ball_uuid'],
}
)
async def group_send_event(self, data):
await self.send(text_data=json.dumps(data))
# 接收前端信息
async def receive(self, text_data):
data = json.loads(text_data)
event = data['event']
if event == "create_player":
await self.create_player(data)
elif event == "move_to":
await self.move_to(data)
elif event == "shoot_fireball":
await self.shoot_fireball(data)
玩家类调用
else if (e.which === 1) {
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
if (outer.cur_skill === "fireball") {
let fireball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === "multi mode") {
outer.playground.mps.send_shoot_fireball(tx, ty, fireball.uuid);
}
}
outer.cur_skill = null; // 释放技能后清空
}
攻击 attack
判断的实现
是否攻击到玩家,只在子弹发射者的窗口内判断,其他玩家接收 server
发来的被攻击到信息即可,因此在火球的碰撞判断中只更新自己发出来火球是否碰撞。
wss
前端
receive () {
let outer = this;
this.ws.onmessage = function(e) {
let data = JSON.parse(e.data);
let uuid = data.uuid;
// 判断是否是自己
if (uuid === outer.uuid) return false;
let event = data.event;
if (event === "create_player") {
outer.receive_create_player(uuid, data.username, data.photo);
} else if(event === "move_to"){
outer.receive_move_to(uuid, data.tx, data.ty);
}else if(event === "shoot_fireball"){
outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.ball_uuid);
}else if(event === "attack"){
outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.angle, data.damage, data.ball_uuid);
}
};
}
send_attack(attackee_uuid, x, y, angle, damage, ball_uuid) {
let outer = this;
this.ws.send(JSON.stringify({
'event': "attack",
'uuid': outer.uuid,
'attackee_uuid': attackee_uuid,
'x': x,
'y': y,
'angle': angle,
'damage': damage,
'ball_uuid': ball_uuid,
}));
}
// x, y 矫正被攻击者位置
receive_attack(uuid, attackee_uuid, x, y, angle, damage, ball_uuid){
let attacker = this.get_player(uuid); // 攻击者
let attackee = this.get_player(attackee_uuid); // 被攻击者
if(attacker && attackee) {
attackee.receive_attack(x, y, angle, damage, ball_uuid, attacker)
}
}
wss
后端
async def attack(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_send_event",
'event': "attack",
'attackee_uuid': data["attackee_uuid"],
'uuid': data['uuid'],
'x': data['x'],
'y': data['y'],
'angle': data['angle'],
'damage': data['damage'],
'ball_uuid': data['ball_uuid'],
}
)
async def group_send_event(self, data):
await self.send(text_data=json.dumps(data))
# 接收前端信息
async def receive(self, text_data):
data = json.loads(text_data)
event = data['event']
if event == "create_player":
await self.create_player(data)
elif event == "move_to":
await self.move_to(data)
elif event == "shoot_fireball":
await self.shoot_fireball(data)
elif event == "attack":
await self.attack(data)
调用:火球类中攻击玩家时调用
game/static/js/src/playground/skill/fireball/zbase.js
attack(player) {
let angle = Math.atan2(player.y - this.y, player.x - this.x);
player.is_attacked(angle, this.damage);
if (this.playground.mode === "multi mode") {
this.playground.mps.send_attack(player.uuid, player.x, player.y, angle, this.damage, this.uuid);
}
this.destroy();
}
https://cdn.acwing.com/media/article/image/2021/12/02/1_9340c86053-fireball.png
https://cdn.acwing.com/media/article/image/2021/12/02/1_daccabdc53-blink.png
状态机机制与公告牌
游戏状态:waiting -> fighting(超过三人) -> over
。
保证公平,对局开始前不允许移动。
在 playground
类中的 show(mode)
函数中设置开始的状态为 waiting
show(mode) { // 打开playground界面
let outer = this;
this.$playground.show();
this.width = this.$playground.width();
this.height = this.$playground.height();
this.game_map = new GameMap(this);
this.mode = mode;
this.state = "waiting"; // waiting -> fighting -> over
this.notic_board = new NoticeBoard(this);
this.player_count = 0;
……
game/static/js/src/playground/notice_board/zbase.js
class NoticeBoard extends HyldObject {
constructor(playground) {
super();
this.playground = playground;
this.ctx = this.playground.game_map.ctx;
this.text = "已就绪:0人";
}
start() {
}
write(text) {
this.text = text;
}
update() {
this.render();
}
// canvas渲染文本
render() {
this.ctx.font = "20px serif";
this.ctx.fillStyle = "white";
this.ctx.textAlign = "center";
this.ctx.fillText(this.text, this.playground.width / 2, 20);
}
}
状态切换
game/static/js/src/playground/palyer/zbase.js
start() {
this.playground.player_count++;
this.playground.notic_board.write("已就绪: " + this.playground.player_count + "人");
if(this.playground.player_count >= 3){
this.playground.state = "fighting";
this.playground.notic_board.write("Fighting");
}
……
技能冷却
玩家不能有无限子弹,所以在在player
对象设置fireball_coldtime
为技能冷却时间,没到时间限制住监听事件add_listening_events
发射和更新update
函数里面随时更新技能冷却时间即可。
game/static/js/src/playground/skill/fireball/zbase.js
技能下方显示代码
if (this.character === "me") {
this.fireball_coldtime = 3; // 单位:秒s
this.fireball_img = new Image();
this.fireball_img.src = "https://cdn.acwing.com/media/article/image/2021/12/02/1_9340c86053-fireball.png"
}
// 技能绘画
render_skill_coldtime() {
let scale = this.playground.scale;
let x = 1.5, y = 0.9, r = 0.04; // 单位scale是hight, 宽的最大为16/9 = 1.7...
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(x * scale, y * scale, r * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.fireball_img, (x - r) * scale, (y - r) * scale, r * 2 * scale, r * 2 * scale);
this.ctx.restore();
if (this.fireball_coldtime > 0) {
this.ctx.beginPath();
this.ctx.moveTo(x * scale, y * scale); // 移到圆心
this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - this.fireball_coldtime / 3) - Math.PI / 2, true);
this.ctx.lineTo(x * scale, y * scale); // 画直线
this.ctx.fillStyle = "rgba(0, 0, 255, 0.6)";
this.ctx.fill();
}
}
调试技巧
删除掉所有的前端调试时用到的输出
console.log(***)
:
- 在js目录下利用
ag
命令搜索cosole.log
,根据输出的文件进行删除- vim进入到各个文件中,
/console/log
在全文中搜索,dd
删除整行,n
进行下一个查找。- 依次将每个文件中的
console
语句删除。
闪现技能 blink
实现该技能很简单,仿照fire_ball
即可
blink(tx, ty) {
let d = this.get_dist(this.x, this.y, tx, ty);
d = Math.min(d, 0.8);
let angle = Math.atan2(ty - this.y, tx - this.x);
this.x += d * Math.cos(angle);
this.y += d * Math.sin(angle);
this.blink_coldtime = 5;
this.move_length = 0; // 闪现完停下来
}
具体修改请查看gitee
提交:implement multi mode and blink and coldtime.