Учебные заметки Django по реализации онлайн-сражений (Часть 2)

Содержание примечаний воспроизведено из раздаточного материала курса AcWing Django Framework, ссылка на курс: Курс AcWing Django Framework .

1. Напишите функцию синхронизации перемещения move_to.

Подобно функции синхронизации из предыдущей главы create_player, синхронизацию мобильной функции также необходимо реализовать во внешнем интерфейсе send_move_toи receive_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_toWebSocket~/djangoapp/game/consumers/multiplayer/index.pyreceiveeventmove_tomove_toMultiPlayerSocketreceivereceive_move_touuidmove_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;
            }
        }
    }

    ...
}

Поскольку у огненного шара Playerесть копия, нам нужно удалить ее, прежде чем удалять Playerогненный шар fire_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_fireballи receive_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;
            }
        }
    }
}

3. Напишите атаку функции синхронизации определения попадания.

Нам нужно унифицировать атакующее действие и использовать одно окно, чтобы однозначно определить попадание. Если оно попадет, оно будет транслироваться в другие окна. Поэтому огненные шары, запускаемые другими игроками в окне, являются лишь анимацией, и не должно быть никаких решимость ударить. Давайте сначала 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();
    }

    ...
}

Каждому игроку также нужна функция 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);
    }

    ...
}

Мы предполагаем, что игрок attacker, который запускает огненный шар, - , а игрок, в которого попали, - . attackeeПоложение пораженного также определяется окном атакующего, и огненный шар также должен исчезнуть в окнах других игроков после попадания в других игроков, поэтому Также необходимо метать огненные шары uuid. Мы MultiPlayerSocketреализуем send_attackфункцию и receive_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);
        }
    }
}

Затем реализуйте бэкэнд-функцию следующим образом:

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)

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() {
    
    
        ...
    }
}

4. Оптимизация и улучшение (доска подсказок игрока, компакт-диск навыков)

Ограничиваем игроков в перемещении, когда количество людей в комнате не достигло 3. Нам нужно AcGamePlaygroundдобавить в класс конечный автомат state. Всего есть три состояния: waiting, fighting, over, и статус каждого окна независим. Подсказка плата будет реализована позже. :

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

        ...
    }

    ...
}

Затем мы реализуем доску подсказок, чтобы показать, сколько игроков ждут в текущей комнате. Создайте новый каталог ~/djangoapp/game/static/js/src/playgroundв каталоге , а затем войдите в каталог, чтобы создать файл, следующим образом:notice_boardzbase.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);
    }
}

Каждый раз, когда создается игрок, player_countчисло будет увеличиваться на 1. Когда количество игроков больше или равно 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;

            ...
        });
    }

    ...
}

Сейчас возможность атаковать в начале игры явно не подходит, поэтому необходимо еще и настроить так, чтобы он не мог атаковать в первые секунды игры, то есть умение остывает. Каждое окно имеет только свое время восстановления навыка, то есть видно только свое время восстановления. Теперь мы устанавливаем время восстановления навыка «Огненный шар» в одну секунду и 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();
    }

    ...
}

Мы до сих пор не знаем, когда навык остынет, поэтому нам нужно добавить значок навыка и подсказку о компакт-диске. Мы можем имитировать другие игры 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;
            }
        }
    }
}

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

Затем нам также необходимо синхронизировать флеш-навыки в многопользовательском режиме.Принцип тот же, что и синхронизация движения.Сначала 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);
        }
    }
}

Затем реализуйте бэкэнд и ~/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)

Наконец, Playerпросто вызовите функцию навыка трансляции flash в классе:

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

    ...
}

おすすめ

転載: blog.csdn.net/m0_51755720/article/details/133273444