笔记内容转载自 AcWing 的 Django 框架课讲义,课程链接:AcWing Django 框架课。
聊天系统整体可以分为两部分:输入框与历史记录。
我们需要先修改一下之前代码中的一个小 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); // 将滚动条移动到最底部
}
...
}
我们先在 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;
}
});
}
...
}