笔记内容转载自 AcWing 的 Django 框架课讲义,课程链接:AcWing Django 框架课。
与上一章中的 create_player
同步函数相似,移动函数的同步也需要在前端实现 send_move_to
和 receive_move_to
函数。我们修改 MultiPlayerSocket
类(在目录 ~/djangoapp/game/static/js/src/playground/socket/multiplayer
中):
class MultiPlayerSocket {
constructor(playground) {
this.playground = playground;
// 直接将网站链接复制过来,将https改成wss,如果没有配置https那就改成ws,然后最后加上wss的路由
this.ws = new WebSocket('wss://app4007.acapp.acwing.com.cn/wss/multiplayer/');
this.start();
}
start() {
this.receive();
}
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);
}
};
}
send_create_player(username, avatar) {
...
}
receive_create_player(uuid, username, avatar) {
...
}
// 根据uuid找到对应的Player
get_player(uuid) {
let players = this.playground.players;
for (let i = 0; i < players.length; i++) {
let player = players[i];
if (player.uuid === uuid)
return player;
}
return null;
}
send_move_to(tx, ty) {
let outer = this;
this.ws.send(JSON.stringify({
'event': 'move_to',
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
}));
}
receive_move_to(uuid, tx, ty) {
let player = this.get_player(uuid);
if (player) { // 确保玩家存在再调用move_to函数
player.move_to(tx, ty);
}
}
}
然后修改一下后端通信代码(~/djangoapp/game/consumers/multiplayer
目录中的 index.py
文件):
from channels.generic.websocket import AsyncWebsocketConsumer
import json
from django.conf import settings
from django.core.cache import cache
class MultiPlayer(AsyncWebsocketConsumer):
async def connect(self):
...
async def disconnect(self, close_code):
...
async def create_player(self, data): # async表示异步函数
...
async def group_send_event(self, data): # 组内的每个连接接收到消息后直接发给前端即可
await self.send(text_data=json.dumps(data))
async def move_to(self, data): # 与create_player函数相似
await self.channel_layer.group_send(
self.room_name,
{
'type': 'group_send_event',
'event': 'move_to',
'uuid': data['uuid'],
'tx': data['tx'],
'ty': data['ty'],
}
)
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)
最后我们还需要调用函数,首先我们需要在 AcGamePlayground
类中记录下游戏模式 mode
:
class AcGamePlayground {
...
// 显示playground界面
show(mode) {
...
this.mode = mode; // 需要将模式记录下来,之后玩家在不同的模式中需要调用不同的函数
this.resize(); // 界面打开后需要resize一次,需要将game_map也resize
...
}
...
}
然后在 Player
类中进行修改,当为多人模式时,需要广播发送 move_to
信号:
class Player extends AcGameObject {
...
add_listening_events() {
let outer = this;
this.playground.game_map.$canvas.on('contextmenu', function() {
return false;
}); // 取消右键的菜单功能
this.playground.game_map.$canvas.mousedown(function(e) {
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) { // 1表示左键,2表示滚轮,3表示右键
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
outer.move_to(tx, ty); // e.clientX/Y为鼠标点击坐标
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_move_to(tx, ty);
}
} else if (e.which === 1) {
...
}
});
...
}
...
}
现在即可实现多名玩家的同步移动。当 A 窗口中的玩家移动时,首先该窗口(Player
类)的监听函数会控制该玩家自身进行移动,接着判定为多人模式,因此再调用 MultiPlayerSocket
类中的 send_move_to
函数向服务器发送信息(通过 WebSocket
向服务器发送一个事件),接着服务器端(~/djangoapp/game/consumers/multiplayer/index.py
文件中)的 receive
函数会接收到信息,发现事件 event
为 move_to
,就会调用 move_to
函数,该函数会向这个房间中的其他所有玩家群发消息,每个窗口都会在前端(MultiPlayerSocket
类中)的 receive
函数接收到信息,通过事件路由到 receive_move_to
函数,该函数就会通过 uuid
调用每名玩家的 move_to
函数。
由于发射的火球是会消失的,因此需要先将每名玩家发射的火球存下来,此外我们实现一个根据火球的 uuid
删除火球的函数,在 Player
类中进行修改:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
this.fire_balls = []; // 存下玩家发射的火球
...
}
...
// 向(tx, ty)位置发射火球
shoot_fireball(tx, ty) {
let x = this.x, y = this.y;
let radius = 0.01;
let theta = Math.atan2(ty - this.y, tx - this.x);
let vx = Math.cos(theta), vy = Math.sin(theta);
let color = 'orange';
let speed = 0.5;
let move_length = 0.8;
let fire_ball = new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, 0.01);
this.fire_balls.push(fire_ball);
return fire_ball; // 返回fire_ball是为了获取自己创建这个火球的uuid
}
destroy_fireball(uuid) { // 删除火球
for (let i = 0; i < this.fire_balls.length; i++) {
let fire_ball = fire_balls[i];
if (fire_ball.uuid === uuid) {
fire_ball.destroy();
break;
}
}
}
...
}
由于火球在 Player
中存了一份,因此我们在删除火球前需要将它从 Player
的 fire_balls
中删掉。且由于 FireBall
类中的 update
函数过于臃肿,可以先将其分成 update_move
以及 update_attack
,我们修改 FireBall
类:
class FireBall extends AcGameObject {
// 火球需要标记是哪个玩家发射的,且射出后的速度方向与大小是固定的,射程为move_length
constructor(playground, player, x, y, radius, vx, vy, color, speed, move_length, damage) {
...
}
start() {
}
update_move() {
let true_move = Math.min(this.move_length, this.speed * this.timedelta / 1000);
this.x += this.vx * true_move;
this.y += this.vy * true_move;
this.move_length -= true_move;
}
update_attack() { // 攻击碰撞检测
for (let i = 0; i < this.playground.players.length; i++) {
let player = this.playground.players[i];
if (player !== this.player && this.is_collision(player)) {
this.attack(player); // this攻击player
}
}
}
update() {
if (this.move_length < this.eps) {
this.destroy();
return false;
}
this.update_move();
this.update_attack();
this.render();
}
get_dist(x1, y1, x2, y2) {
...
}
is_collision(player) {
...
}
attack(player) {
...
}
render() {
...
}
on_destroy() {
let fire_balls = this.player.fire_balls;
for (let i = 0; i < fire_balls.length; i++) {
if (fire_balls[i] === this) {
fire_balls.splice(i, 1);
break;
}
}
}
}
然后我们在 MultiPlayerSocket
类中实现 send_shoot_fireball
和 receive_shoot_fireball
函数:
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);
}
};
}
...
send_shoot_fireball(tx, ty, fireball_uuid) {
let outer = this;
this.ws.send(JSON.stringify({
'event': 'shoot_fireball',
'uuid': outer.uuid,
'tx': tx,
'ty': ty,
'fireball_uuid': fireball_uuid,
}));
}
receive_shoot_fireball(uuid, tx, ty, fireball_uuid) {
let player = this.get_player(uuid);
if (player) {
let fire_ball = player.shoot_fireball(tx, ty);
fire_ball.uuid = fireball_uuid; // 所有窗口同一个火球的uuid需要统一
}
}
}
现在我们需要实现后端函数:
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from django.conf import settings
from django.core.cache import cache
class MultiPlayer(AsyncWebsocketConsumer):
...
async def shoot_fireball(self, data):
await self.channel_layer.group_send(
self.room_name,
{
'type': 'group_send_event',
'event': 'shoot_fireball',
'uuid': data['uuid'],
'tx': data['tx'],
'ty': data['ty'],
'fireball_uuid': data['fireball_uuid'],
}
)
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)
最后是在 Player
类中调用函数:
class Player extends AcGameObject {
constructor(playground, x, y, radius, color, speed, character, username, avatar) {
...
}
start() {
...
}
add_listening_events() {
let outer = this;
this.playground.game_map.$canvas.on('contextmenu', function() {
return false;
}); // 取消右键的菜单功能
this.playground.game_map.$canvas.mousedown(function(e) {
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) { // 1表示左键,2表示滚轮,3表示右键
...
} else if (e.which === 1) {
let tx = (e.clientX - rect.left) / outer.playground.scale;
let ty = (e.clientY - rect.top) / outer.playground.scale;
if (outer.cur_skill === 'fireball') {
let fire_ball = outer.shoot_fireball(tx, ty);
if (outer.playground.mode === 'multi mode') {
outer.playground.mps.send_shoot_fireball(tx, ty, fire_ball.uuid);
}
}
outer.cur_skill = null; // 释放完一次技能后还原
}
});
$(window).keydown(function(e) {
if (e.which === 81) { // Q键
outer.cur_skill = 'fireball';
return false;
}
});
}
// 计算两点之间的欧几里得距离
get_dist(x1, y1, x2, y2) {
...
}
// 向(tx, ty)位置发射火球
shoot_fireball(tx, ty) {
...
}
destroy_fireball(uuid) { // 删除火球
...
}
move_to(tx, ty) {
...
}
is_attacked(theta, damage) { // 被攻击到
...
}
// 更新移动
update_move() {
...
}
update() {
...
}
render() {
...
}
on_destroy() {
for (let i = 0; i < this.playground.players.length; i++) {
if (this.playground.players[i] === this) {
this.playground.players.splice(i, 1);
break;
}
}
}
}