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


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

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

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

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

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

    class GameMap extends AcGameObject {
        constructor(playground) {  // 需要将AcGamePlayground传进来
            super();  // 调用基类构造函数,相当于将自己添加到了AC_GAME_OBJECTS中
            this.playground = playground;
            this.$canvas = $(``);  // 画布,用来渲染画面,tabindex=0表示能够监听事件
            ...
        }
    
        start() {
            this.$canvas.focus();  // 聚焦后才能监听事件
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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;
                }
            });
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    聊天的前端界面需要创建一个新的文件,我们在 ~/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上 } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    然后在 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);
                };
            }
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    现在在 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;
                }
            });
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    然后我们还需要实现一下聊天区的 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

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

    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); // 将滚动条移动到最底部 } ... }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    然后实现后端代码:

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    最后在前端的 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;
                }
            });
        }
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
  • 相关阅读:
    全新特征平台 FeatInsight 测试平台上线,现已开放抢先体验!
    【第95题】JAVA高级技术-网络编程14(简易聊天室9:使用Socket传递音频)
    Socket网络编程(三)——TCP快速入门
    C/C++语言100题练习计划 73——Error(二分答案算法实现)
    设计模式(一)| 创建型模式(单例模式、原型模式等)
    安装opensearch
    Linux —— 进程概念超详解!(持续更新……)
    Vue3 如何实现一个全局搜索框
    SSM整合流程(整合配置、功能模块开发、接口测试)
    Registry Keys for Windows 10 Application Privacy Settings
  • 原文地址:https://blog.csdn.net/m0_51755720/article/details/133544401