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

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

CONTENTS

    • [1. 实现聊天系统前端界面](#1. 实现聊天系统前端界面)
    • [2. 实现后端同步函数](#2. 实现后端同步函数)

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

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

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

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

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

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

    ...
}

Player 类中修改:

js 复制代码
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 文件:

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

        this.$history = $(`<div class='ac_game_chat_field_history'></div>`);
        this.$input = $(`<input type='text' class='ac_game_chat_field_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 类中创建出来:

js 复制代码
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 类中即可监听事件:

js 复制代码
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 文件中):

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

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

js 复制代码
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 $(`<div>${message}</div>`);
    }

    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 前端实现发送和接收消息的函数:

js 复制代码
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);
    }
}

然后实现后端代码:

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 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 类中调用一下发送消息的函数即可:

js 复制代码
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;
            }
        });
    }

    ...
}
相关推荐
eeee~~几秒前
GeoPandas在地理空间数据分析中的应用
python·jupyter·信息可视化·数据分析·geopandas库
重生之我要进大厂几秒前
LeetCode 876
java·开发语言·数据结构·算法·leetcode
Amo Xiang17 分钟前
Python 常用模块(四):shutil模块
开发语言·python
HinsCoder23 分钟前
【渗透测试】——Upload靶场实战(1-5关)
笔记·学习·安全·web安全·渗透测试·测试·upload靶场
听风若依26 分钟前
排序学习笔记
笔记·学习
Filotimo_30 分钟前
【自然语言处理】实验三:新冠病毒的FAQ问答系统
人工智能·经验分享·笔记·python·学习·自然语言处理·pycharm
Happy鱿鱼34 分钟前
C语言-数据结构 有向图拓扑排序TopologicalSort(邻接表存储)
c语言·开发语言·数据结构
KBDYD101035 分钟前
C语言--结构体变量和数组的定义、初始化、赋值
c语言·开发语言·数据结构·算法
IM_DALLA37 分钟前
【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL21
学习·fpga开发
计算机学姐37 分钟前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea