• Django学习笔记-实现联机对战(下)


    笔记内容转载自 AcWing 的 Django 框架课讲义,课程链接:AcWing Django 框架课

    1. 编写移动同步函数move_to

    与上一章中的 create_player 同步函数相似,移动函数的同步也需要在前端实现 send_move_toreceive_move_to 函数。我们修改 MultiPlayerSocket 类(在目录 ~/djangoapp/game/static/js/src/playground/socket/multiplayer 中):

    class MultiPlayerSocket {
        constructor(playground) {
            this.playground = playground;
    
            // 直接将网站链接复制过来,将https改成wss,如果没有配置https那就改成ws,然后最后加上wss的路由
            this.ws = new WebSocket('wss://app4007.acapp.acwing.com.cn/wss/multiplayer/');
    
            this.start();
        }
    
        start() {
            this.receive();
        }
    
        receive() {
            let outer = this;
    
            this.ws.onmessage = function(e) {
                let data = JSON.parse(e.data);  // 将字符串变回JSON
                let uuid = data.uuid;
                if (uuid === outer.uuid) return false;  // 如果是给自己发送消息就直接过滤掉
    
                let event = data.event;
                if (event === 'create_player') {  // create_player路由
                    outer.receive_create_player(uuid, data.username, data.avatar);
                } else if (event === 'move_to') {  // move_to路由
                    outer.receive_move_to(uuid, data.tx, data.ty);
                }
            };
        }
    
        send_create_player(username, avatar) {
            ...
        }
    
        receive_create_player(uuid, username, avatar) {
            ...
        }
    
        // 根据uuid找到对应的Player
        get_player(uuid) {
            let players = this.playground.players;
            for (let i = 0; i < players.length; i++) {
                let player = players[i];
                if (player.uuid === uuid)
                    return player;
            }
            return null;
        }
    
        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) {  // 确保玩家存在再调用move_to函数
                player.move_to(tx, ty);
            }
        }
    }
    
    • 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

    然后修改一下后端通信代码(~/djangoapp/game/consumers/multiplayer 目录中的 index.py 文件):

    from channels.generic.websocket import AsyncWebsocketConsumer
    import json
    from django.conf import settings
    from django.core.cache import cache
    
    class MultiPlayer(AsyncWebsocketConsumer):
        async def connect(self):
            ...
    
        async def disconnect(self, close_code):
            ...
    
    
        async def create_player(self, data):  # async表示异步函数
            ...
    
        async def group_send_event(self, data):  # 组内的每个连接接收到消息后直接发给前端即可
            await self.send(text_data=json.dumps(data))
    
        async def move_to(self, data):  # 与create_player函数相似
            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 receive(self, text_data):
            data = json.loads(text_data)
            print(data)
    
            event = data['event']
            if event == 'create_player':  # 做一个路由
                await self.create_player(data)
            elif event == 'move_to':  # move_to的路由
                await self.move_to(data)
    
    • 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

    最后我们还需要调用函数,首先我们需要在 AcGamePlayground 类中记录下游戏模式 mode

    class AcGamePlayground {
        ...
    
        // 显示playground界面
        show(mode) {
            ...
    
            this.mode = mode;  // 需要将模式记录下来,之后玩家在不同的模式中需要调用不同的函数
    
            this.resize();  // 界面打开后需要resize一次,需要将game_map也resize
    
            ...
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    然后在 Player 类中进行修改,当为多人模式时,需要广播发送 move_to 信号:

    class Player extends AcGameObject {
        ...
    
        add_listening_events() {
            let outer = this;
            this.playground.game_map.$canvas.on('contextmenu', function() {
                return false;
            });  // 取消右键的菜单功能
            this.playground.game_map.$canvas.mousedown(function(e) {
                const rect = outer.ctx.canvas.getBoundingClientRect();
                if (e.which === 3) {  // 1表示左键,2表示滚轮,3表示右键
                    let tx = (e.clientX - rect.left) / outer.playground.scale;
                    let ty = (e.clientY - rect.top) / outer.playground.scale;
                    outer.move_to(tx, ty);  // e.clientX/Y为鼠标点击坐标
    
                    if (outer.playground.mode === 'multi mode') {
                        outer.playground.mps.send_move_to(tx, ty);
                    }
                } else if (e.which === 1) {
                    ...
                }
            });
            ...
        }
    
        ...
    }
    
    • 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

    现在即可实现多名玩家的同步移动。当 A 窗口中的玩家移动时,首先该窗口(Player 类)的监听函数会控制该玩家自身进行移动,接着判定为多人模式,因此再调用 MultiPlayerSocket 类中的 send_move_to 函数向服务器发送信息(通过 WebSocket 向服务器发送一个事件),接着服务器端(~/djangoapp/game/consumers/multiplayer/index.py 文件中)的 receive 函数会接收到信息,发现事件 eventmove_to,就会调用 move_to 函数,该函数会向这个房间中的其他所有玩家群发消息,每个窗口都会在前端(MultiPlayerSocket 类中)的 receive 函数接收到信息,通过事件路由到 receive_move_to 函数,该函数就会通过 uuid 调用每名玩家的 move_to 函数。

    2. 编写攻击同步函数shoot_fireball

    由于发射的火球是会消失的,因此需要先将每名玩家发射的火球存下来,此外我们实现一个根据火球的 uuid 删除火球的函数,在 Player 类中进行修改:

    class Player extends AcGameObject {
        constructor(playground, x, y, radius, color, speed, character, username, avatar) {
            ...
    
            this.fire_balls = [];  // 存下玩家发射的火球
    
            ...
        }
    
        ...
    
        // 向(tx, ty)位置发射火球
        shoot_fireball(tx, ty) {
            let x = this.x, y = this.y;
            let radius = 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 = 0.5;
            let move_length = 0.8;
            let fire_ball = new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, 0.01);
            this.fire_balls.push(fire_ball);
            return fire_ball;  // 返回fire_ball是为了获取自己创建这个火球的uuid
        }
    
    	destroy_fireball(uuid) {  // 删除火球
            for (let i = 0; i < this.fire_balls.length; i++) {
                let fire_ball = this.fire_balls[i];
                if (fire_ball.uuid === uuid) {
                    fire_ball.destroy();
                    break;
                }
            }
        }
    
        ...
    }
    
    • 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

    由于火球在 Player 中存了一份,因此我们在删除火球前需要将它从 Playerfire_balls 中删掉。且由于 FireBall 类中的 update 函数过于臃肿,可以先将其分成 update_move 以及 update_attack,我们修改 FireBall 类:

    class FireBall extends AcGameObject {
        // 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
        constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
            ...
        }
    
        start() {
        }
    
        update_move() {
            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;
        }
    
        update_attack() {  // 攻击碰撞检测
            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
                }
            }
        }
    
        update() {
            if (this.move_length < this.eps) {
                this.destroy();
                return false;
            }
    
            this.update_move();
            this.update_attack();
    
            this.render();
        }
    
        get_dist(x1, y1, x2, y2) {
            ...
        }
    
        is_collision(player) {
            ...
        }
    
        attack(player) {
            ...
        }
    
        render() {
            ...
        }
    
        on_destroy() {
            let fire_balls = this.player.fire_balls;
            for (let i = 0; i < fire_balls.length; i++) {
                if (fire_balls[i] === this) {
                    fire_balls.splice(i, 1);
                    break;
                }
            }
        }
    }
    
    • 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

    然后我们在 MultiPlayerSocket 类中实现 send_shoot_fireballreceive_shoot_fireball 函数:

    class MultiPlayerSocket {
        ...
    
        receive() {
            let outer = this;
    
            this.ws.onmessage = function(e) {
                let data = JSON.parse(e.data);  // 将字符串变回JSON
                let uuid = data.uuid;
                if (uuid === outer.uuid) return false;  // 如果是给自己发送消息就直接过滤掉
    
                let event = data.event;
                if (event === 'create_player') {  // create_player路由
                    outer.receive_create_player(uuid, data.username, data.avatar);
                } else if (event === 'move_to') {  // move_to路由
                    outer.receive_move_to(uuid, data.tx, data.ty);
                } else if (event === 'shoot_fireball') {  // shoot_fireball路由
                    outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.fireball_uuid);
                }
            };
        }
    
        ...
    
        send_shoot_fireball(tx, ty, fireball_uuid) {
            let outer = this;
            this.ws.send(JSON.stringify({
                'event': 'shoot_fireball',
                'uuid': outer.uuid,
                'tx': tx,
                'ty': ty,
                'fireball_uuid': fireball_uuid,
            }));
        }
    
        receive_shoot_fireball(uuid, tx, ty, fireball_uuid) {
            let player = this.get_player(uuid);
            if (player) {
                let fire_ball = player.shoot_fireball(tx, ty);
                fire_ball.uuid = fireball_uuid;  // 所有窗口同一个火球的uuid需要统一
            }
        }
    }
    
    • 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

    现在我们需要实现后端函数:

    import json
    from channels.generic.websocket import AsyncWebsocketConsumer
    from django.conf import settings
    from django.core.cache import cache
    
    class MultiPlayer(AsyncWebsocketConsumer):
        ...
    
        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'],
                    'fireball_uuid': data['fireball_uuid'],
                }
            )
    
    
        async def receive(self, text_data):
            data = json.loads(text_data)
            print(data)
    
            event = data['event']
            if event == 'create_player':  # 做一个路由
                await self.create_player(data)
            elif event == 'move_to':  # move_to的路由
                await self.move_to(data)
            elif event == 'shoot_fireball':  # shoot_fireball的路由
                await self.shoot_fireball(data)
    
    • 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

    最后是在 Player 类中调用函数:

    class Player extends AcGameObject {
        constructor(playground, x, y, radius, color, speed, character, username, avatar) {
            ...
        }
    
        start() {
            ...
        }
    
        add_listening_events() {
            let outer = this;
            this.playground.game_map.$canvas.on('contextmenu', function() {
                return false;
            });  // 取消右键的菜单功能
            this.playground.game_map.$canvas.mousedown(function(e) {
                const rect = outer.ctx.canvas.getBoundingClientRect();
                if (e.which === 3) {  // 1表示左键,2表示滚轮,3表示右键
                    ...
                } 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 fire_ball = outer.shoot_fireball(tx, ty);
    
                        if (outer.playground.mode === 'multi mode') {
                            outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
                        }
                    }
    
                    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) {
            ...
        }
    
        // 向(tx, ty)位置发射火球
        shoot_fireball(tx, ty) {
            ...
        }
    
        destroy_fireball(uuid) {  // 删除火球
            ...
        }
    
        move_to(tx, ty) {
            ...
        }
    
        is_attacked(theta, damage) {  // 被攻击到
            ...
        }
    
        // 更新移动
        update_move() {
            ...
        }
    
        update() {
            ...
        }
    
        render() {
            ...
        }
    
        on_destroy() {
            for (let i = 0; i < this.playground.players.length; i++) {
                if (this.playground.players[i] === this) {
                    this.playground.players.splice(i, 1);
                    break;
                }
            }
        }
    }
    
    • 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

    3. 编写击中判定同步函数attack

    我们需要统一攻击这个动作,由一个窗口来唯一判断是否击中,若击中则广播给其他窗口,因此窗口中看到其他玩家发射的火球仅为动画,不应该有击中判定。我们先在 FireBall 类中进行修改:

    class FireBall extends AcGameObject {
        ...
    
        update() {
            if (this.move_length < this.eps) {
                this.destroy();
                return false;
            }
    
            this.update_move();
    
            if (this.player.character !== 'enemy') {  // 在敌人的窗口中不进行攻击检测
                this.update_attack();
            }
    
            this.render();
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    每名玩家还需要有一个函数 receive_attack 表示接收到被攻击的信息:

    class Player extends AcGameObject {
        ...
    
        destroy_fireball(uuid) {  // 删除火球
            for (let i = 0; i < this.fire_balls.length; i++) {
                let fire_ball = this.fire_balls[i];
                if (fire_ball.uuid === uuid) {
                    fire_ball.destroy();
                    break;
                }
            }
        }
    
        ...
    
        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 < this.eps) {  // 半径小于eps认为已死
                this.destroy();
                return false;
            }
            this.damage_vx = Math.cos(theta);
            this.damage_vy = Math.sin(theta);
            this.damage_speed = damage * 90;
        }
    
        receive_attack(x, y, theta, damage, fireball_uuid, attacker) {  // 接收被攻击到的消息
            attacker.destroy_fireball(fireball_uuid);
            this.x = x;
            this.y = y;
            this.is_attacked(theta, damage);
        }
    
        ...
    }
    
    • 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

    我们假设发射火球的玩家为 attacker,被击中的玩家为 attackee,被击中者的位置也是由攻击者的窗口决定的,且火球在击中其他玩家后在其他玩家的窗口也应该消失,因此还需要传火球的 uuid。我们在 MultiPlayerSocket 类中实现 send_attackreceive_attack 函数:

    class MultiPlayerSocket {
        ...
    
        receive() {
            let outer = this;
    
            this.ws.onmessage = function(e) {
                let data = JSON.parse(e.data);  // 将字符串变回JSON
                let uuid = data.uuid;
                if (uuid === outer.uuid) return false;  // 如果是给自己发送消息就直接过滤掉
    
                let event = data.event;
                if (event === 'create_player') {  // create_player路由
                    outer.receive_create_player(uuid, data.username, data.avatar);
                } else if (event === 'move_to') {  // move_to路由
                    outer.receive_move_to(uuid, data.tx, data.ty);
                } else if (event === 'shoot_fireball') {  // shoot_fireball路由
                    outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.fireball_uuid);
                } else if (event === 'attack') {  // attack路由
                    outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.theta, data.damage, data.fireball_uuid);
                }
            };
        }
    
        ...
    
        send_attack(attackee_uuid, x, y, theta, damage, fireball_uuid) {
            let outer = this;
            this.ws.send(JSON.stringify({
                'event': 'attack',
                'uuid': outer.uuid,
                'attackee_uuid': attackee_uuid,
                'x': x,
                'y': y,
                'theta': theta,
                'damage': damage,
                'fireball_uuid': fireball_uuid,
            }));
        }
    
        receive_attack(uuid, attackee_uuid, x, y, theta, damage, fireball_uuid) {
            let attacker = this.get_player(uuid);
            let attackee = this.get_player(attackee_uuid);
            if (attacker && attackee) {  // 如果攻击者和被攻击者都还存在就判定攻击
                attackee.receive_attack(x, y, theta, damage, fireball_uuid, attacker);
            }
        }
    }
    
    • 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

    然后实现后端函数如下:

    import json
    from channels.generic.websocket import AsyncWebsocketConsumer
    from django.conf import settings
    from django.core.cache import cache
    
    class MultiPlayer(AsyncWebsocketConsumer):
        ...
    
        async def attack(self, data):
            await self.channel_layer.group_send(
                self.room_name,
                {
                    'type': 'group_send_event',
                    'event': 'attack',
                    'uuid': data['uuid'],
                    'attackee_uuid': data['attackee_uuid'],
                    'x': data['x'],
                    'y': data['y'],
                    'theta': data['theta'],
                    'damage': data['damage'],
                    'fireball_uuid': data['fireball_uuid'],
                }
            )
    
    
        async def receive(self, text_data):
            data = json.loads(text_data)
            print(data)
    
            event = data['event']
            if event == 'create_player':  # 做一个路由
                await self.create_player(data)
            elif event == 'move_to':  # move_to的路由
                await self.move_to(data)
            elif event == 'shoot_fireball':  # shoot_fireball的路由
                await self.shoot_fireball(data)
            elif event == 'attack':  # attack的路由
                await self.attack(data)
    
    • 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

    最后需要在火球 FireBall 类中调用攻击判定的同步函数:

    class FireBall extends AcGameObject {
        // 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
        constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
            ...
        }
    
        start() {
        }
    
        update_move() {
            ...
        }
    
        update_attack() {  // 攻击碰撞检测
            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
                }
            }
        }
    
        update() {
            if (this.move_length < this.eps) {
                this.destroy();
                return false;
            }
    
            this.update_move();
    
            if (this.player.character !== 'enemy') {  // 在敌人的窗口中不进行攻击检测
                this.update_attack();
            }
    
            this.render();
        }
    
        get_dist(x1, y1, x2, 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);
    
            if (this.playground.mode === 'multi mode') {
                this.playground.mps.send_attack(player.uuid, player.x, player.y, theta, this.damage, this.uuid);
            }
    
            this.destroy();
        }
    
        render() {
            ...
        }
    
        on_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
    • 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

    4. 优化改进(玩家提示板、技能CD)

    我们限制在房间人数还没到3个时玩家不能移动,需要在 AcGamePlayground 类中添加一个状态机 state,一共有三种状态:waitingfightingover,且每个窗口的状态是独立的,提示板会在之后进行实现:

    class AcGamePlayground {
        ...
    
        // 显示playground界面
        show(mode) {
            ...
    
            this.mode = mode;  // 需要将模式记录下来,之后玩家在不同的模式中需要调用不同的函数
            this.state = 'waiting';  // waiting -> fighting -> over
            this.notice_board = new NoticeBoard(this);  // 提示板
            this.player_count = 0;  // 玩家人数
    
            this.resize();  // 界面打开后需要resize一次,需要将game_map也resize
    
            ...
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    接下来我们实现一个提示板,显示当前房间有多少名玩家在等待,在 ~/djangoapp/game/static/js/src/playground 目录下新建 notice_board 目录,然后进入该目录创建 zbase.js 文件如下:

    class NoticeBoard extends AcGameObject {
        constructor(playground) {
            super();
    
            this.playground = playground;
            this.ctx = this.playground.game_map.ctx;
            this.text = '已就绪: 0人';
        }
    
        start() {
        }
    
        write(text) {  // 更新this.text的信息
            this.text = text;
        }
    
        update() {
            this.render();
        }
    
        render() {  // Canvas渲染文本
            this.ctx.font = '20px serif';
            this.ctx.fillStyle = 'white';
            this.ctx.textAlign = 'center';
            this.ctx.fillText(this.text, this.playground.width / 2, 20);
        }
    }
    
    • 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

    每次有玩家创建时就将 player_count 的数量加一,当玩家数量大于等于3时将游戏状态转换成 Fighting,且设置除了在 Fighting 状态下点击鼠标或按下按键才有效果,否则无效。在 Player 类中进行修改:

    class Player extends AcGameObject {
        ...
    
        start() {
            this.playground.player_count++;
            this.playground.notice_board.write('已就绪: ' + this.playground.player_count + '人');
    
            if (this.playground.player_count >= 3) {
                this.playground.state = 'fighting';
                this.playground.notice_board.write('Fighting');
            }
    
            ...
        }
    
        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 (outer.playground.state !== 'fighting')
                    return false;  // 点击事件不往后传
    
                ...
                }
            });
            $(window).keydown(function(e) {
                if (outer.playground.state !== 'fighting')
                    return true;
    
                ...
            });
        }
    
        ...
    }
    
    • 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

    现在对局一开始就能攻击,显然不太合适,因此还需要设定在游戏刚开始的前若干秒无法攻击,即技能冷却。每个窗口只有自己才有技能冷却,也就是只能看到自己的冷却时间。现在我们给火球技能设置一秒的冷却时间,在 Player 类中进行修改:

    class Player extends AcGameObject {
        constructor(playground, x, y, radius, color, speed, character, username, avatar) {
            ...
    
            if (this.character === 'me') {  // 如果是自己的话则加上技能CD
                this.fireball_coldtime = 1;  // 单位: s
            }
        }
    
        ...
    
        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 (outer.playground.state !== 'fighting')
                    return false;  // 点击事件不往后传
    
                const rect = outer.ctx.canvas.getBoundingClientRect();
                if (e.which === 3) {  // 1表示左键,2表示滚轮,3表示右键
                    let tx = (e.clientX - rect.left) / outer.playground.scale;
                    let ty = (e.clientY - rect.top) / outer.playground.scale;
                    outer.move_to(tx, ty);  // e.clientX/Y为鼠标点击坐标
    
                    if (outer.playground.mode === 'multi mode') {
                        outer.playground.mps.send_move_to(tx, ty);
                    }
                } 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 fire_ball = outer.shoot_fireball(tx, ty);
    
                        if (outer.playground.mode === 'multi mode') {
                            outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
                        }
    
                        outer.fireball_coldtime = 1;  // 用完技能后重置冷却时间
                    }
    
                    outer.cur_skill = null;  // 释放完一次技能后还原
                }
            });
            $(window).keydown(function(e) {
                if (outer.playground.state !== 'fighting')
                    return true;
    
                if (e.which === 81 && outer.fireball_coldtime < outer.eps) {  // Q键
                    outer.cur_skill = 'fireball';
                    return false;
                }
            });
        }
    
        ...
    
        update_coldtime() {  // 更新技能冷却时间
            this.fireball_coldtime -= this.timedelta / 1000;
            this.fireball_coldtime = Math.max(this.fireball_coldtime, 0);  // 防止变为负数
        }
    
        update() {
            this.spent_time += this.timedelta / 1000;  // 将这行代码从update_move函数移动到update函数中
            this.update_move();
            if (this.character === 'me' && this.playground.state === 'fighting') {  // 只有自己且开始战斗后才更新冷却时间
                this.update_coldtime();
            }
            this.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

    我们还不知道技能什么时候冷却好,因此还需要加上一个技能图标与 CD 提示,可以模仿其他 MOBA 类游戏,在技能图标上添加一层 CD 涂层即可。假设我们的技能图标资源存放在 ~/djangoapp/game/static/image/playground 目录下,那么我们在 Player 类中渲染技能图标:

    class Player extends AcGameObject {
        constructor(playground, x, y, radius, color, speed, character, username, avatar) {
            ...
    
            if (this.character === 'me') {  // 如果是自己的话则加上技能CD
                this.fireball_coldtime = 1;  // 单位: s
                this.fireball_img = new Image();
                this.fireball_img.src = 'https://app4007.acapp.acwing.com.cn/static/image/playground/fireball.png';  // 技能图标资源链接
            }
        }
    
        ...
    
        render() {
            let scale = this.playground.scale;  // 要将相对值恢复成绝对值
            if (this.character !== 'robot') {
                this.ctx.save();
                this.ctx.beginPath();
                this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
                this.ctx.stroke();
                this.ctx.clip();
                this.ctx.drawImage(this.img, (this.x - this.radius) * scale, (this.y - this.radius) * scale, this.radius * 2 * scale, this.radius * 2 * scale);
                this.ctx.restore();
            } else {  // AI
                this.ctx.beginPath();
                // 角度从0画到2PI,是否逆时针为false
                this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
                this.ctx.fillStyle = this.color;
                this.ctx.fill();
            }
    
            if (this.character === 'me' && this.playground.state === 'fighting') {
                this.render_skill_coldtime();
            }
        }
    
        render_skill_coldtime() {  // 渲染技能图标与冷却时间
            let x = 1.5, y = 0.95, r = 0.03;
            let scale = this.playground.scale;
    
            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();
                // 角度由冷却时间决定
                let fireball_coldtime_ratio = this.fireball_coldtime / 1;  // 剩余冷却时间占总冷却时间的比例
                this.ctx.moveTo(x * scale, y * scale);  // 设置圆心从(x, y)开始画
                // 减去PI/2的目的是为了从PI/2处开始转圈,而不是从0度开始
                // 最后的参数为false为取逆时针方向,反之为顺时针,但为true后相当于绘制的是冷却时间对立的另一段,因此需要调换一下冷却时间
                this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - fireball_coldtime_ratio) - Math.PI / 2, true);
                this.ctx.lineTo(x * scale, y * scale);  // 画完之后向圆心画一条线
                this.ctx.fillStyle = 'rgba(0, 0, 255, 0.6)';
                this.ctx.fill();
            }
        }
    
        on_destroy() {
            if (this.character === 'me')
                this.playground.state = 'over';  // 玩家寄了之后更新状态为over
    
            for (let i = 0; i < this.playground.players.length; i++) {
                if (this.playground.players[i] === this) {
                    this.playground.players.splice(i, 1);
                    break;
                }
            }
        }
    }
    
    • 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

    5. 闪现技能

    闪现技能的实现很简单,整体参考之前的火球技能即可,我们先实现单机模式下的闪现技能,在 Player 类中实现:

    class Player extends AcGameObject {
        constructor(playground, x, y, radius, color, speed, character, username, avatar) {
            ...
    
            if (this.character === 'me') {  // 如果是自己的话则加上技能CD
                this.fireball_coldtime = 1;  // 单位: s
                this.fireball_img = new Image();
                this.fireball_img.src = 'https://app4007.acapp.acwing.com.cn/static/image/playground/fireball.png';  // 技能图标资源链接
    
                this.blink_coldtime = 10;  // 闪现技能冷却时间
                this.blink_img = new Image();
                this.blink_img.src = 'https://app4007.acapp.acwing.com.cn/static/image/playground/blink.png';
            }
        }
    
        ...
    
        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 (outer.playground.state !== 'fighting')
                    return false;  // 点击事件不往后传
    
                const rect = outer.ctx.canvas.getBoundingClientRect();
                if (e.which === 3) {  // 1表示左键,2表示滚轮,3表示右键
                    let tx = (e.clientX - rect.left) / outer.playground.scale;
                    let ty = (e.clientY - rect.top) / outer.playground.scale;
                    outer.move_to(tx, ty);  // e.clientX/Y为鼠标点击坐标
    
                    if (outer.playground.mode === 'multi mode') {
                        outer.playground.mps.send_move_to(tx, ty);
                    }
                } 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 fire_ball = outer.shoot_fireball(tx, ty);
    
                        if (outer.playground.mode === 'multi mode') {
                            outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
                        }
    
                        outer.fireball_coldtime = 1;  // 用完技能后重置冷却时间
                    } else if (outer.cur_skill === 'blink') {
                        outer.blink(tx, ty);
                        outer.blink_coldtime = 10;
                    }
    
                    outer.cur_skill = null;  // 释放完一次技能后还原
                }
            });
            $(window).keydown(function(e) {
                if (outer.playground.state !== 'fighting')
                    return true;
    
                if (e.which === 81 && outer.fireball_coldtime < outer.eps) {  // Q键
                    outer.cur_skill = 'fireball';
                    return false;
                } else if (e.which === 70 && outer.blink_coldtime < outer.eps) {  // F键
                    outer.cur_skill = 'blink';
                    return false;
                }
            });
        }
    
        // 计算两点之间的欧几里得距离
        get_dist(x1, y1, x2, y2) {
            return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        }
    
        ...
    
        blink(tx, ty) {  // 闪现到(tx, ty)
            let x = this.x, y = this.y;
            let dist = this.get_dist(x, y, tx, ty);
            dist = Math.min(dist, 0.3);  // 最大闪现距离为0.3
            let theta = Math.atan2(ty - y, tx - x);
            this.x += dist * Math.cos(theta);
            this.y += dist * Math.sin(theta);
    
            this.move_length = 0;  // 闪现完之后应该停下来而不是继续移动
        }
    
        ...
    
        update_coldtime() {  // 更新技能冷却时间
            this.fireball_coldtime -= this.timedelta / 1000;
            this.fireball_coldtime = Math.max(this.fireball_coldtime, 0);  // 防止变为负数
    
            this.blink_coldtime -= this.timedelta / 1000;
            this.blink_coldtime = Math.max(this.blink_coldtime, 0);
        }
    
        update() {
            this.spent_time += this.timedelta / 1000;  // 将这行代码从update_move函数移动到update函数中
            this.update_move();
            if (this.character === 'me' && this.playground.state === 'fighting') {  // 只有自己且开始战斗后才更新冷却时间
                this.update_coldtime();
            }
            this.render();
        }
    
        render() {
            let scale = this.playground.scale;  // 要将相对值恢复成绝对值
            if (this.character !== 'robot') {
                this.ctx.save();
                this.ctx.beginPath();
                this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
                this.ctx.stroke();
                this.ctx.clip();
                this.ctx.drawImage(this.img, (this.x - this.radius) * scale, (this.y - this.radius) * scale, this.radius * 2 * scale, this.radius * 2 * scale);
                this.ctx.restore();
            } else {  // AI
                this.ctx.beginPath();
                // 角度从0画到2PI,是否逆时针为false
                this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
                this.ctx.fillStyle = this.color;
                this.ctx.fill();
            }
    
            if (this.character === 'me' && this.playground.state === 'fighting') {
                this.render_fireball_coldtime();
                this.render_blink_coldtime();
            }
        }
    
        render_fireball_coldtime() {  // 渲染火球技能图标与冷却时间
            let x = 1.5, y = 0.95, r = 0.03;
            let scale = this.playground.scale;
    
            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();
                // 角度由冷却时间决定
                let coldtime_ratio = this.fireball_coldtime / 1;  // 剩余冷却时间占总冷却时间的比例
                this.ctx.moveTo(x * scale, y * scale);  // 设置圆心从(x, y)开始画
                // 减去PI/2的目的是为了从PI/2处开始转圈,而不是从0度开始
                // 最后的参数为false为取逆时针方向,反之为顺时针,但为true后相当于绘制的是冷却时间对立的另一段,因此需要调换一下冷却时间
                this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - coldtime_ratio) - Math.PI / 2, true);
                this.ctx.lineTo(x * scale, y * scale);  // 画完之后向圆心画一条线
                this.ctx.fillStyle = 'rgba(0, 0, 255, 0.6)';
                this.ctx.fill();
            }
        }
    
        render_blink_coldtime() {  // 渲染闪现技能图标与冷却时间
            let x = 1.6, y = 0.95, r = 0.03;
            let scale = this.playground.scale;
    
            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.blink_img, (x - r) * scale, (y - r) * scale, r * 2 * scale, r * 2 * scale);
            this.ctx.restore();
    
            if (this.blink_coldtime > 0) {
                this.ctx.beginPath();
                let coldtime_ratio = this.blink_coldtime / 10;
                this.ctx.moveTo(x * scale, y * scale);  // 设置圆心从(x, y)开始画
                this.ctx.arc(x * scale, y * scale, r * scale, 0 - Math.PI / 2, Math.PI * 2 * (1 - coldtime_ratio) - Math.PI / 2, true);
                this.ctx.lineTo(x * scale, y * scale);  // 画完之后向圆心画一条线
                this.ctx.fillStyle = 'rgba(0, 0, 255, 0.6)';
                this.ctx.fill();
            }
        }
    
        on_destroy() {
            if (this.character === 'me')
                this.playground.state = 'over';  // 玩家寄了之后更新状态为over
    
            for (let i = 0; i < this.playground.players.length; i++) {
                if (this.playground.players[i] === this) {
                    this.playground.players.splice(i, 1);
                    break;
                }
            }
        }
    }
    
    • 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

    然后我们还需要将闪现技能在多人模式中进行同步,原理和移动的同步是一样的,先在 MultiPlayerSocket 类中实现前端函数:

    class MultiPlayerSocket {
        ...
    
        receive() {
            let outer = this;
    
            this.ws.onmessage = function(e) {
                let data = JSON.parse(e.data);  // 将字符串变回JSON
                let uuid = data.uuid;
                if (uuid === outer.uuid) return false;  // 如果是给自己发送消息就直接过滤掉
    
                let event = data.event;
                if (event === 'create_player') {  // create_player路由
                    outer.receive_create_player(uuid, data.username, data.avatar);
                } else if (event === 'move_to') {  // move_to路由
                    outer.receive_move_to(uuid, data.tx, data.ty);
                } else if (event === 'shoot_fireball') {  // shoot_fireball路由
                    outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.fireball_uuid);
                } else if (event === 'attack') {  // attack路由
                    outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.theta, data.damage, data.fireball_uuid);
                } else if (event === 'blink') {  // blink路由
                    outer.receive_blink(uuid, data.tx, data.ty);
                }
            };
        }
    
        ...
    
        send_blink(tx, ty) {
            let outer = this;
            this.ws.send(JSON.stringify({
                'event': 'blink',
                'uuid': outer.uuid,
                'tx': tx,
                'ty': ty,
            }));
        }
    
        receive_blink(uuid, tx, ty) {
            let player = this.get_player(uuid);
            if (player) {
                player.blink(tx, ty);
            }
        }
    }
    
    • 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

    然后实现一下后端,在 ~/djangoapp/game/consumers/multiplayer/index.py 文件中实现:

    import json
    from channels.generic.websocket import AsyncWebsocketConsumer
    from django.conf import settings
    from django.core.cache import cache
    
    class MultiPlayer(AsyncWebsocketConsumer):
        ...
    
        async def blink(self, data):
            await self.channel_layer.group_send(
                self.room_name,
                {
                    'type': 'group_send_event',
                    'event': 'blink',
                    'uuid': data['uuid'],
                    'tx': data['tx'],
                    'ty': data['ty'],
                }
            )
    
    
        async def receive(self, text_data):
            data = json.loads(text_data)
            print(data)
    
            event = data['event']
            if event == 'create_player':  # 做一个路由
                await self.create_player(data)
            elif event == 'move_to':  # move_to的路由
                await self.move_to(data)
            elif event == 'shoot_fireball':  # shoot_fireball的路由
                await self.shoot_fireball(data)
            elif event == 'attack':  # attack的路由
                await self.attack(data)
            elif event == 'blink':  # blink的路由
                await self.blink(data)
    
    • 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

    最后在 Player 类中调用一下广播闪现技能的函数即可:

    class Player extends AcGameObject {
        ...
    
        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 (outer.playground.state !== 'fighting')
                    return false;  // 点击事件不往后传
    
                const rect = outer.ctx.canvas.getBoundingClientRect();
                if (e.which === 3) {  // 1表示左键,2表示滚轮,3表示右键
                    let tx = (e.clientX - rect.left) / outer.playground.scale;
                    let ty = (e.clientY - rect.top) / outer.playground.scale;
                    outer.move_to(tx, ty);  // e.clientX/Y为鼠标点击坐标
    
                    if (outer.playground.mode === 'multi mode') {
                        outer.playground.mps.send_move_to(tx, ty);
                    }
                } 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 fire_ball = outer.shoot_fireball(tx, ty);
    
                        if (outer.playground.mode === 'multi mode') {
                            outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
                        }
    
                        outer.fireball_coldtime = 1;  // 用完技能后重置冷却时间
                    } else if (outer.cur_skill === 'blink') {
                        outer.blink(tx, ty);
    
                        if (outer.playground.mode === 'multi mode') {
                            outer.playground.mps.send_blink(tx, ty);
                        }
    
                        outer.blink_coldtime = 10;
                    }
    
                    outer.cur_skill = null;  // 释放完一次技能后还原
                }
            });
            $(window).keydown(function(e) {
                if (outer.playground.state !== 'fighting')
                    return true;
    
                if (e.which === 81 && outer.fireball_coldtime < outer.eps) {  // Q键
                    outer.cur_skill = 'fireball';
                    return false;
                } else if (e.which === 70 && outer.blink_coldtime < outer.eps) {  // F键
                    outer.cur_skill = 'blink';
                    return false;
                }
            });
        }
    
        ...
    }
    
    • 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
  • 相关阅读:
    pytorch单精度、半精度、混合精度、单卡、多卡(DP / DDP)、FSDP、DeepSpeed模型训练
    冒泡排序/鸡尾酒排序
    2021论文阅读笔记集合
    视频产品介绍:AS-VCVR-N多协议视频接入网关
    力扣215. 数组中的第K个最大元素
    天然产物在新冠中的应用潜力 | MedChemExpress
    (iView)表格JSON字符串转为键值对,去掉对象的双引号和花括号,省略号隐藏,悬浮显示完整内容
    std::queue 中遇到释放内存错误的问题
    Gin学习记录4——Controller和中间件
    基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理
  • 原文地址:https://blog.csdn.net/m0_51755720/article/details/133273444