7、联机对战下

源码地址https://gitee.com/pxlsdz/hyld

演示地址http://121.199.59.80/hyld/

目标:实现剩下的三个同步函数

  1. create player通过 server 同步玩家信息,有几个玩家,各自的状态如何。(上文实现)
  2. move to同步移动,某个玩家的移动需要通知其他玩家,在其他玩家的窗口渲染
  3. shoot fireball火球发射,与同步玩家状态类似,同步火球的状态
  4. 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 中实现 fireballsendreceive 函数

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(***) :

  1. 在js目录下利用 ag 命令搜索 cosole.log ,根据输出的文件进行删除
  2. vim进入到各个文件中, /console/log 在全文中搜索, dd 删除整行, n 进行下一个查找。
  3. 依次将每个文件中的 console 语句删除。

实现该技能很简单,仿照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.

posted @ 2022-01-02 19:18  pxlsdz  阅读(52)  评论(0编辑  收藏  举报