Django学习笔记-实现聊天系统

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

CONTENTS

    • 1. 实现聊天系统前端界面
    • 2. 实现后端同步函数

1. 实现聊天系统前端界面

聊天系统整体可以分为两部分:输入框与历史记录。

我们需要先修改一下之前代码中的一个小 BUG,当在一个窗口中按 Q 时,另一个窗口中点击鼠标左键也能攻击,因为按下按键的事件被所有窗口都捕捉到了,这是不合理的。

我们之前监听的对象是 window,每个地图是一个 canvas 元素,因此我们可以绑定到 canvas 对象上。由于不是所有对象都能添加绑定事件的,因此我们还需要对 canvas 做一个修改,首先在 GameMap 类中修改一下 canvas 对象:

class GameMap extends AcGameObject {
    constructor(playground) {  // 需要将AcGamePlayground传进来
        super();  // 调用基类构造函数,相当于将自己添加到了AC_GAME_OBJECTS中
        this.playground = playground;
        this.$canvas = $(``);  // 画布,用来渲染画面,tabindex=0表示能够监听事件
        ...
    }

    start() {
        this.$canvas.focus();  // 聚焦后才能监听事件
    }

    ...
}

Player 类中修改:

class Player extends AcGameObject {
    ...

    add_listening_events() {
        let outer = this;

        ...

        this.playground.game_map.$canvas.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;
            }
        });
    }

    ...
}

聊天的前端界面需要创建一个新的文件,我们在 ~/djangoapp/game/static/js/src/playground 目录下创建一个 chat_field 目录,并进入该目录创建 zbase.js 文件:

class ChatField {
    constructor(playground) {
        this.playground = playground;
        this.func_id = null;  // 在每次打开输入框时需要将之前历史记录框的计时函数删掉

        this.$history = $(`
`
); this.$input = $(``); this.$history.hide(); this.$input.hide(); this.playground.$playground.append(this.$history); this.playground.$playground.append(this.$input); this.start(); } start() { this.add_listening_events(); } add_listening_events() { let outer = this; this.$input.keydown(function(e) { // 输入框也需要监听ESC事件 if (e.which === 27) { outer.hide_input(); return false; } }); } show_history() { let outer = this; this.$history.fadeIn(); // 慢慢显示出来 if (this.func_id) clearTimeout(this.func_id); this.func_id = setTimeout(function() { outer.$history.fadeOut(); outer.func_id = null; }, 3000); // 显示3秒后消失 } show_input() { this.$input.show(); this.show_history(); // 打开输入框顺带打开历史记录 this.$input.focus(); // 聚焦一下才能输入 } hide_input() { this.$input.hide(); this.playground.game_map.$canvas.focus(); // 关闭输入框后要重新聚焦回Canvas上 } }

然后在 AcGamePlayground 类中创建出来:

class AcGamePlayground {
    ...

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

        // 单人模式下创建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.07, this.get_random_color(), 0.15, 'robot'));
            }
        } else if (mode === 'multi mode') {
            this.mps = new MultiPlayerSocket(this);
            this.mps.uuid = this.players[0].uuid;  // 用每名玩家的唯一编号区分不同的窗口

            this.chat_field = new ChatField(this);  // 聊天区

            this.mps.ws.onopen = function() {
                outer.mps.send_create_player(outer.root.settings.username, outer.root.settings.avatar);
            };
        }
    }

    ...
}

现在在 Player 类中即可监听事件:

class Player extends AcGameObject {
    ...

    add_listening_events() {
        let outer = this;

        ...

        this.playground.game_map.$canvas.keydown(function(e) {
            if (e.which === 13 && outer.playground.mode === 'multi mode') {  // 还没满人允许使用聊天功能
                outer.playground.chat_field.show_input();
                return false;
            } else if (e.which === 27 && outer.playground.mode === 'multi mode') {
                outer.playground.chat_field.hide_input();
                return false;
            }

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

    ...
}

然后我们还需要实现一下聊天区的 CSS 样式(在 ~/djangoapp/game/static/css 目录的 game.css 文件中):

...

.ac_game_chat_field_history {
    position: absolute;
    top: 40%;
    left: 15%;
    transform: translate(-50%, 50%);
    width: 20%;
    height:30%;
    color: white;
    background-color: rgba(77, 77, 77, 0.2);
    font-size: 1.5vh;
    padding: 5px;
    overflow: auto;
}

.ac_game_chat_field_history::-webkit-scrollbar {  /* 滚动条 */
    width: 1;
}

.ac_game_chat_field_input {
    position: absolute;
    top: 86%;
    left: 15%;
    transform: translate(-50%, 50%);
    width: 20%;
    height: 2vh;
    color: white;
    background-color: rgba(222, 225, 230, 0.2);
    font-size: 1.5vh;
}

现在我们实现在历史记录区域里添加新消息的功能:

class ChatField {
    constructor(playground) {
        ...
    }

    start() {
        this.add_listening_events();
    }

    add_listening_events() {
        let outer = this;

        this.$input.keydown(function(e) {  // 输入框也需要监听ESC事件
            if (e.which === 27) {
                outer.hide_input();
                return false;
            } else if (e.which === 13) {  // 按Enter键时发送消息
                let username = outer.playground.root.settings.username;
                let text = outer.$input.val();
                outer.hide_input();  // 发送完消息后关闭输入框
                if (text) {  // 信息不为空才渲染出来
                    outer.$input.val('');  // 将输入框清空
                    outer.add_message(username, text);
                }
                return false;
            }
        });
    }

    render_message(message) {  // 渲染消息
        return $(`
${message}
`
); } add_message(username, text) { // 向历史记录区里添加消息 let message = `[${username}] ${text}`; this.$history.append(this.render_message(message)); this.show_history(); // 每次发新消息时都显示一下历史记录 this.$history.scrollTop(this.$history[0].scrollHeight); // 将滚动条移动到最底部 } ... }

2. 实现后端同步函数

我们先在 WebSocket 前端实现发送和接收消息的函数:

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);
            } else if (event === 'message') {  // message路由
                outer.receive_message(data.username, data.text);
            }
        };
    }

    ...

    send_message(username, text) {
        let outer = this;
        this.ws.send(JSON.stringify({
            'event': 'message',
            'uuid': outer.uuid,
            'username': username,
            'text': text,
        }));
    }

    receive_message(username, text) {
        this.playground.chat_field.add_message(username, text);
    }
}

然后实现后端代码:

import json
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from django.core.cache import cache

class MultiPlayer(AsyncWebsocketConsumer):
    ...

    async def message(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': 'group_send_event',
                'event': 'message',
                'uuid': data['uuid'],
                'username': data['username'],
                'text': data['text'],
            }
        )


    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)
        elif event == 'message':  # message的路由
            await self.message(data)

最后在前端的 ChatField 类中调用一下发送消息的函数即可:

class ChatField {
    ...

    add_listening_events() {
        let outer = this;

        this.$input.keydown(function(e) {  // 输入框也需要监听ESC事件
            if (e.which === 27) {
                outer.hide_input();
                return false;
            } else if (e.which === 13) {  // 按Enter键时发送消息
                let username = outer.playground.root.settings.username;
                let text = outer.$input.val();
                outer.hide_input();  // 发送完消息后关闭输入框
                if (text) {  // 信息不为空才渲染出来
                    outer.$input.val('');  // 将输入框清空
                    outer.add_message(username, text);

                    outer.playground.mps.send_message(username, text);  // 给其他玩家的窗口发送消息
                }
                return false;
            }
        });
    }

    ...
}

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