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

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

CONTENTS

    • 1. 统一长度单位
    • 2. 增加联机对战模式
    • 3. 配置Django Channels

1. 统一长度单位

多人模式中每个玩家所看到的地图相对来说应该是一样的,因此需要固定地图的长宽比,一般固定为16:9。我们需要在游戏窗口的长宽中取最小值,然后将地图渲染为16:9的大小。

我们在 AcGamePlayground 类中实现一个 resize 函数用于将长宽比调整为16:9并且达到最大:

class AcGamePlayground {
    constructor(root) {
        this.root = root;
        this.$playground = $(`
            
`
); this.root.$ac_game.append(this.$playground); this.start(); } get_random_color() { ... } start() { this.hide(); // 初始化时需要先关闭playground界面 let outer = this; $(window).resize(function() { outer.resize(); }); // 用户改变窗口大小时改函数会触发 } // 将长宽比调整为16:9 resize() { this.width = this.$playground.width(); this.height = this.$playground.height(); let unit = Math.min(this.width / 16, this.height / 9); this.width = unit * 16; this.height = unit * 9; this.scale = this.height; // 当窗口大小改变时所有目标的相对大小和位置也要改变 if (this.game_map) this.game_map.resize(); // 如果地图存在需要调用地图的resize函数 } // 显示playground界面 show() { this.$playground.show(); // 将界面的宽高先存下来 this.width = this.$playground.width(); this.height = this.$playground.height(); this.game_map = new GameMap(this); // 创建游戏画面 this.resize(); // 界面打开后需要resize一次,需要将game_map也resize ... } // 关闭playground界面 hide() { this.$playground.hide(); } }

现在需要将窗口大小的修改效果作用到黑色背景上,因此我们在 GameMap 类中也实现一个 resize 函数用于修改背景大小:

class GameMap extends AcGameObject {
    constructor(playground) {  // 需要将AcGamePlayground传进来
        super();  // 调用基类构造函数,相当于将自己添加到了AC_GAME_OBJECTS中
        this.playground = playground;
        this.$canvas = $(``);  // 画布,用来渲染画面
        this.ctx = this.$canvas[0].getContext('2d');  // 二维画布
        this.ctx.canvas.width = this.playground.width;  // 设置画布宽度
        this.ctx.canvas.height = this.playground.height;  // 设置画布高度
        this.playground.$playground.append(this.$canvas);  // 将画布添加到HTML中
    }

    start() {
    }

    resize() {
        this.ctx.canvas.width = this.playground.width;
        this.ctx.canvas.height = this.playground.height;
        this.ctx.fillStyle = 'rgba(0, 0, 0, 1)';  // 每次调整大小后直接涂一层不透明的背景
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }

    update() {
        this.render();  // 每一帧都要画一次
    }

    render() {
        this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';  // 黑色背景
        // 左上角坐标(0, 0),右下角坐标(w, h)
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }
}

我们修改一下 game.css 文件,添加以下内容,实现将地图居中:

.ac_game_playground > canvas {
    position: relative;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

现在我们还需要修改地图里面的目标,一共有三种分别是玩家、火球、被击中的粒子效果。

首先修改一下 AcGamePlayground 类中的玩家初始化代码:

class AcGamePlayground {
    constructor(root) {
        ...
    }

    get_random_color() {
        ...
    }

    start() {
        ...
    }

    // 将长宽比调整为16:9
    resize() {
        ...
    }

    // 显示playground界面
    show() {
        this.$playground.show();

        // 将界面的宽高先存下来
        this.width = this.$playground.width();
        this.height = this.$playground.height();

        this.game_map = new GameMap(this);  // 创建游戏画面

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

        this.players = [];  // 所有玩家
        this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, 'white', 0.15, true));  // 创建自己

        // 创建敌人
        for (let i = 0; i < 8; i++) {
            this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, this.get_random_color(), 0.15, false));
        }
    }

    // 关闭playground界面
    hide() {
        this.$playground.hide();
    }
}

然后我们修改 Player 类,将所有绝对变量替换为相对变量:

class Player extends AcGameObject {
    constructor(playground, x, y, radius, color, speed, is_me) {
        ...
        this.eps = 0.01;  // 误差小于0.01认为是0
        ...
    }

    start() {
        if (this.is_me) {
            this.add_listening_events();
        } else {
            // Math.random()返回一个0~1之间的数,随机初始化AI的位置
            let tx = Math.random() * this.playground.width / this.playground.scale;
            let ty = Math.random() * this.playground.height / this.playground.scale;
            this.move_to(tx, ty);
        }
    }

    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表示右键
                outer.move_to((e.clientX - rect.left) / outer.playground.scale, (e.clientY - rect.top) / outer.playground.scale);  // e.clientX/Y为鼠标点 击坐标
            } else if (e.which === 1) {
                if (outer.cur_skill === 'fireball') {
                    outer.shoot_fireball((e.clientX - rect.left) / outer.playground.scale, (e.clientY - rect.top) / outer.playground.scale);
                }

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

    move_to(tx, ty) {
        ...
    }

    is_attacked(theta, damage) {  // 被攻击到
        ...
    }

    // 更新移动
    update_move() {
        this.spent_time += this.timedelta / 1000;
        // AI敌人随机向玩家射击,游戏刚开始前三秒AI不能射击
        if (this.spent_time > 3 && !this.is_me && Math.random() < 1 / 360.0) {
            let player = this.playground.players[0];
            this.shoot_fireball(player.x, player.y);
        }

        if (this.damage_speed > this.eps) {  // 有击退效果时玩家无法移动
            this.vx = this.vy = 0;
            this.move_length = 0;
            this.x += this.damage_vx * this.damage_speed * this.timedelta / 1000;
            this.y += this.damage_vy * this.damage_speed * this.timedelta / 1000;
            this.damage_speed *= this.friction;
        } else {
            if (this.move_length < this.eps) {
                this.move_length = 0;
                this.vx = this.vy = 0;
                if (!this.is_me) {  // AI敌人不能停下来
                    let tx = Math.random() * this.playground.width / this.playground.scale;
                    let ty = Math.random() * this.playground.height / this.playground.scale;
                    this.move_to(tx, ty);
                }
            } else {
                // 计算真实移动距离,与一帧的移动距离取min防止移出界
                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() {
        this.update_move();
        this.render();
    }

    render() {
        let scale = this.playground.scale;  // 要将相对值恢复成绝对值
        if (this.is_me) {
            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();
        }
    }
}

然后修改 FireBall 类,只需要修改 eps 以及 render 函数即可:

class FireBall extends AcGameObject {
    // 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
    constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
        ...
        this.eps = 0.01;
    }

    start() {
    }

    update() {
        ...
    }

    get_dist(x1, y1, x2, y2) {
        ...
    }

    is_collision(player) {
        ...
    }

    attack(player) {
        ...
    }

    render() {
        let scale = this.playground.scale;
        this.ctx.beginPath();
        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();
    }
}

最后修改 Particle 类,同样也是只需要修改 eps 以及 render 函数即可:

class Particle extends AcGameObject {
    constructor(playground, x, y, radius, vx, vy, color, speed, move_length) {
        ...
        this.eps = 0.01;
        this.friction = 0.9;
    }

    start() {
    }

    update() {
        ...
    }

    render() {
        let scale = this.playground.scale;
        this.ctx.beginPath();
        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();
    }
}

2. 增加联机对战模式

我们先修改 AcGameMenu 类,实现多人模式按钮的逻辑:

class AcGameMenu {
    constructor(root) {  // root用来传AcGame对象
        ...
    }

    start() {
        this.hide();
        this.add_listening_events();
    }

    // 给按钮绑定监听函数
    add_listening_events() {
        let outer = this;
        // 注意在function中调用this指的是function本身,因此需要先将外面的this存起来
        this.$single.click(function() {
            outer.hide();  // 关闭menu界面
            outer.root.playground.show('single mode');  // 显示playground界面,加入参数用于区分
        });
        this.$multi.click(function() {
            outer.hide();
            outer.root.playground.show('multi mode');  // 多人模式
        });
        this.$settings.click(function() {
            outer.root.settings.logout_on_remote();
        });
    }

    // 显示menu界面
    show() {
        this.$menu.show();
    }

    // 关闭menu界面
    hide() {
        this.$menu.hide();
    }
}

然后修改 AcGamePlayground 类,区分两种模式,且需要进一步区分玩家类别,之前使用 True/False 表示是否是玩家本人,现在可以用字符串区分玩家本人、其他玩家以及人机:

class AcGamePlayground {
    constructor(root) {
        ...
    }

    get_random_color() {
        ...
    }

    start() {
        ...
    }

    // 将长宽比调整为16:9
    resize() {
        ...
    }

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

        // 将界面的宽高先存下来
        this.width = this.$playground.width();
        this.height = this.$playground.height();

        this.game_map = new GameMap(this);  // 创建游戏画面

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

        this.players = [];  // 所有玩家
        this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, 'white', 0.15, 'me', this.root.settings.username, this.root.settings.avatar));  // 创建自己,自己的用户名和头像从settings中获得

        // 单人模式下创建AI敌人
        if (mode === 'single mode'){
            for (let i = 0; i < 8; i++) {
                this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, this.get_random_color(), 0.15, 'robot'));
            }
        } else if (mode === 'multi mode') {
        }
    }

    // 关闭playground界面
    hide() {
        this.$playground.hide();
    }
}

然后还需要修改一下 Player 类,将原本的 this.is_me 判断进行修改:

class Player extends AcGameObject {
    constructor(playground, x, y, radius, color, speed, character, username, avatar) {
        ...
        this.character = character;
        this.username = username;
        this.avatar = avatar;
        ...
        if (this.character !== 'robot') {  // 只有AI不用渲染图片
            this.img = new Image();
            this.img.src = this.avatar;
        }
    }

    start() {
        if (this.character === 'me') {  // 只给自己添加监听函数
            this.add_listening_events();
        } else {
            ...
        }
    }

    add_listening_events() {
        ...
    }

    // 计算两点之间的欧几里得距离
    get_dist(x1, y1, x2, y2) {
        ...
    }

    // 向(tx, ty)位置发射火球
    shoot_fireball(tx, ty) {
        ...
    }

    move_to(tx, ty) {
        ...
    }

    is_attacked(theta, damage) {  // 被攻击到
        ...
    }

    // 更新移动
    update_move() {
        this.spent_time += this.timedelta / 1000;
        // AI敌人随机向玩家射击,游戏刚开始前三秒AI不能射击
        if (this.character === 'robot' && this.spent_time > 3 && Math.random() < 1 / 360.0) {
            ...
        }

        if (this.damage_speed > this.eps) {  // 有击退效果时玩家无法移动
            ...
        } else {
            if (this.move_length < this.eps) {
                ...
                if (this.character === 'robot') {  // AI敌人不能停下来
                    ...
                }
            } else {
                // 计算真实移动距离,与一帧的移动距离取min防止移出界
                ...
            }
        }
    }

    update() {
        this.update_move();
        this.render();
    }

    render() {
        let scale = this.playground.scale;  // 要将相对值恢复成绝对值
        if (this.character !== 'robot') {
            ...
        } else {  // AI
            ...
        }
    }
}

3. 配置Django Channels

假设有三名玩家编号为1、2、3进行多人游戏,那么每个玩家都有自己的一个窗口,且窗口中都能看到三名玩家。如果当前玩家1、2在进行游戏,3加入了游戏,那么需要告诉1、2两名玩家3来了,且还要告诉3当前已经有玩家1、2了。

要实现这一点,可以通过一个中心服务器(可以就是自己租的云服务器),即3向服务器发送他来了,服务器给1、2发送消息,且服务器给3发送消息说之前已经有1、2两名玩家了。因此服务器中需要存储每个地图中的玩家信息,用于完成第一个同步事件:生成玩家事件。

我们之后一共需要实现四个同步函数:create_playermove_toshoot_fireballattack。前三个函数顾名思义,最后的 attack 函数是因为服务器存在延迟,比如3发射一个火球在本地看打中了1,但是由于延迟在1那边可能是没被打中的。

攻击判断是一个权衡问题,一般的游戏都是选择在本地进行攻击判断,而不是云服务器,即以发起攻击的玩家窗口进行判断,如果击中了则通过 attack 函数在服务器上广播信息。

在此之前我们使用的是 HTTP 协议,该协议为单向的,即客户端需要先向服务器请求信息后服务器才会返回信息,而服务器是不会主动向客户端发送信息的。

因此此处我们需要使用 WebSocket 协议(WS),同理该协议也有对应的加密协议 WSS,Django Channels 即为 Django 支持 WSS 协议的一种实现方式。

你可能感兴趣的:(Django,数据库,django,学习,笔记,ubuntu)