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

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

本节内容是通过 WebSocket 实现多人模式中的在线聊天系统。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 类中修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 文件:

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
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 类中创建出来:

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
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 类中即可监听事件:

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
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 文件中):

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
...

.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
32
33
34
35
36
37
38
39
40
41
42
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 前端实现发送和接收消息的函数:

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

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
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学习笔记-实现联机对战(下)

下一章:无。