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

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

CONTENTS

    • 1. 编写移动同步函数move_to
    • 2. 编写攻击同步函数shoot_fireball

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

然后修改一下后端通信代码(~/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)

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

class AcGamePlayground {
    ...

    // 显示playground界面
    show(mode) {
        ...

        this.mode = mode;  // 需要将模式记录下来,之后玩家在不同的模式中需要调用不同的函数

        this.resize();  // 界面打开后需要resize一次,需要将game_map也resize

        ...
    }

    ...
}

然后在 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) {
                ...
            }
        });
        ...
    }

    ...
}

现在即可实现多名玩家的同步移动。当 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 = fire_balls[i];
            if (fire_ball.uuid === uuid) {
                fire_ball.destroy();
                break;
            }
        }
    }

    ...
}

由于火球在 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;
            }
        }
    }
}

然后我们在 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需要统一
        }
    }
}

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

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)

最后是在 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;
            }
        }
    }
}

你可能感兴趣的:(Django,django,学习,笔记,python,开发语言)