6、联机对战上
统一长度单位
各个玩家的比要统一为16:9
。
在game/static/js/src/playground/zbase.js
文件中编写 resize
函数,并在 start() {}
中增加 $(window).resize(function() { outer.resize(); });
监听窗口事件,便可随时调整玩家的游戏界面宽高比。
resize
函数实现:设置长度单位unit
为宽度1/16
和高度1/9
的最小值,那么宽 = unit * 16
和 高 = unit * 9
就不会溢出了, 并且保证宽高调比为16:9
,在 gamemap
类中添加 resize
函数,使得 canvas
的宽高同步更新。
start() {
let outer = this;
$(window).resize(function () {
outer.resize();
});
}
resize() {
this.width = this.$playground.width();
this.height = this.$playground.height();
let unit = Math.min(this.width / 16, this.height / 9);
this.width = unit * 16;
this.height = unit * 9;
this.scale = this.height; //
if (this.game_map) this.game_map.resize();
}
game/static/js/src/playground/game_map/zbase.js
resize() {
this.ctx.canvas.width = this.playground.width;
this.ctx.canvas.height = this.playground.height;
console.log(this.ctx.canvas.width)
this.ctx.fillStyle = "rgba(0, 0, 0, 1)"; // 每一次 `canvas`更改大小时,直接涂一层不透明的黑色防止渐变色
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
之前的程序为在 show
函数内将生成的 $playground
添加 append
到 $hyld
内 ,由于调整宽高比时需要多次调用 show
函数,因此将 append
语句调整到 HyldPlayground
的构造函数内。
游戏界面居中 :更改 game/static/css/game.css
文件
.hyld-playground {
width: 100%;
height: 100%;
user-select: none;
background-color: grey;
}
.hyld-playground > canvas {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
当缩放游戏界面大小时,游戏内的对象的大小应该也随之改变
游戏界面内的对象分为三种:玩家 player
、火球技能 skill
、粒子效果 particle
将玩家相关的x,y坐标
转换为相对值,在 render
函数中绘图时再转换为当前地图的绝对值。
将子弹的大小也更新为相对值,主要是 render
函数渲染时要确定绝对坐标进行绘制。
具体修改代码见仓库的commit
提交:normalizing unit。
增加联机对战模式
在 game/static/js/src/menu/zbase.js
添加多人模式:
add_listening_events() {
let outer = this;
this.$single_mode.click(function(){
outer.hide();
outer.root.playground.show("single mode");
});
this.$multi_mode.click(function(){
outer.hide();
outer.root.playground.show("multi mode");
});
this.$settings.click(function(){
outer.root.settings.logout_on_remote();
});
}
对player
的构造函数的参数加以修改,参数 is_me
改为 character
表示自己、机器人和其他玩家。
在多人模式时需要获得其他用户的头像和昵称,因此player
构造函数中再添加 username
和 photo
。
添加Django channels
思路
wss用来支持游戏同步以及聊天室功能
中心服务器与客户端需要双向连接,因此 http 协议(单线)不再适用,使用 websocket 协议(双向)。
http
的加密版是 https
, ws
的加密版是 wss
。
用 Django
对 wss
的支持工具: Django Channels
。
同步4个事件
create player
通过server
同步玩家信息,有几个玩家,各自的状态如何。(本文实现)move to
同步移动,某个玩家的移动需要通知其他玩家,在其他玩家的窗口渲染shoot fireball
火球发射,与同步玩家状态类似,同步火球的状态attack
函数,同步有没有被击中,为了减轻服务器负载、提升玩家体验,在本地判定玩家是否被击中,然后再将结果传给服务器,服务器再将结果更新给其他玩家。
配置channels_redis
hyld
为项目名字
1. 安装channels_redis
:
pip install channels_redis
2. 配置hyld/hyld/asgi.py
内容如下:
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from game.routing import websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hyld.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})
3. 配置hyld/hyld/settings.py
在INSTALLED_APPS
中添加channels
,添加后如下所示:
INSTALLED_APPS = [
'channels',
'game.apps.GameConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
然后在文件末尾添加:
ASGI_APPLICATION = 'hyld.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
4. 配置hyld/game/routing.py
这一部分的作用相当于http
的urls
。 内容如下:
from django.urls import path
websocket_urlpatterns = [
]
5. 编写hyld/game/consumers
这一部分的作用相当于http
的views
。
参考示例:
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class MultiPlayer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
print('accept')
self.room_name = "room"
await self.channel_layer.group_add(self.room_name, self.channel_name)
async def disconnect(self, close_code):
print('disconnect')
await self.channel_layer.group_discard(self.room_name, self.channel_name);
async def receive(self, text_data):
data = json.loads(text_data)
print(data)
6. 启动django_channels
在~/project/hyld
目录下执行:
daphne -b 0.0.0.0 -p 5015 hyld.asgi:application
上述为配置一般项。
7.前端发起以及后端做的修改
在 playground
文件夹下创建 socket
文件夹用来存储客户端创建的 websocket
与 server
端建立连接。
在 game/static/js/src/playground/socket/multiplayer/
下创建zbase.js
文件用来创建wss
请求,
class MultiPlayerSocket{
constructor(playground) {
this.playground = playground;
this.ws = new WebSocket("wss://app820.acapp.acwing.com.cn/wss/multiplayer/");
this.start();
}
start(){
}
}
routing.py
创建wss
的路由
from django.urls import path
from game.consumers.multiplayer.index import MultiPlayer
websocket_urlpatterns = [
path("wss/multiplayer/", MultiPlayer.as_asgi(), name="wss_multiplayer"),
]
acapp/game/consumers/multiplayer/index.py
是wss
的后端处理函数,有三个主要函数
accept
函数: 成功创建连接时被调用disconnect
函数 :断开连接时调用receiv
函数 :接收前端向后端发起的请求
当打开多人模式时,在 wss
服务端的界面可以看到 accept
,刷新时会看到 disconnect
127.0.0.1:45232 - - [19/Dec/2021:13:12:11] "WSCONNECT /wss/multiplayer/" - -
accept
127.0.0.1:45232 - - [19/Dec/2021:13:12:29] "WSDISCONNECT /wss/multiplayer/" - -
disconnect
向后端发送消息,等待链接创建成功再发送,创建的socket
中有api
:mps.ws.onopen()
会在连接创建成功时调用。
实现两个同步函数,从前端向服务端发送玩家创建的消息,另一个是服务端接收前端玩家创建的消息。
编写create player
同步函数
所有的玩家和火球需要有唯一的编号,以便进行同步,在 gameobject
中给每个游戏对象添加一个随机生成的8位的 uuid
。
哪个窗口创建了该对象,则 uuid
就使用哪个窗口的。
接收到自己发送的信息时应当忽略,因此需要判断信息是由谁发出的, wss
发送信息时携带 player
的 uuid
.
在 playground
中创建 web socket
对象 mps(multi player socket)
时, 将 mps
的 uuid
设置为 playground
下的游戏玩家数组 players
第一个玩家即 players[0]
的 uuid
, 因为对于每个用户来说,自己总是第一个被加入到 players
数组中,因此 players[0]
即为自己。
后端需要撰写同步函数,确定玩家分配的房间,将玩家和房间信息保存到 redis
中,并设置有效时间为1小时,获取到玩家的 uuid
username
photo
,并发送给前端的各个玩家,前端接收到 wss
的信息时,在自己的界面绘制玩家信息。
后端
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):
self.room_name = None
# 枚举可用房间
for i in range(1000):
name = "room-%d" % i
if not cache.has_key(name) or len(cache.get(name)) < settings.ROOM_CAPACITY:
self.room_name = name
break
if not self.room_name:
return
await self.accept()
# 创建房间
if not cache.has_key(self.room_name):
cache.set(self.room_name, [], 3600) # 房间有效期一小时
# 服务器向房间里其他玩家客户端发送玩家信息
for player in cache.get(self.room_name):
await self.send(text_data=json.dumps({
'event': "create_player",
'uuid': player['uuid'],
'username': player['username'],
'photo': player['photo'],
}))
await self.channel_layer.group_add(self.room_name, self.channel_name)
async def disconnect(self, close_code): # 大概率断开连接时调用
print('disconnect')
await self.channel_layer.group_discard(self.room_name, self.channel_name)
async def create_player(self, data):
players = cache.get(self.room_name)
players.append({
'uuid': data['uuid'],
'username': data['username'],
'photo': data['photo'],
})
cache.set(self.room_name, players, 3600) # 相对于最后一个进入游戏玩家,房间有效期一小时
# 向房间内所有玩家发送
await self.channel_layer.group_send(
self.room_name,
{
'type': "group_create_player", # 重要消息,下方的函数名就为type变量的值
'event': "create_player",
'uuid': data['uuid'],
'username': data['username'],
'photo': data['photo'],
}
)
async def group_create_player(self, data):
await self.send(text_data=json.dumps(data))
# 接收前端信息
async def receive(self, text_data):
data = json.loads(text_data)
event = data['event']
if event == "create_player":
await self.create_player(data)
前端
game/static/js/src/playground/socket/multiplayer/zbase.js
class MultiPlayerSocket {
constructor(playground) {
this.playground = playground;
this.ws = new WebSocket("wss://app820.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);
let uuid = data.uuid;
// 判断是否是自己
if (uuid === outer.uuid) return false;
let event = data.event;
if (event === "create_player") {
outer.receive_create_player(uuid, data.username, data.photo);
}
};
}
// 向服务器发送创建用户消息
send_create_player(username, photo) {
let outer = this;
this.ws.send(JSON.stringify({
'event': "create_player",
'uuid': outer.uuid,
'username': username,
'photo': photo,
}));
}
receive_create_player(uuid, username, photo) {
let player = new Player(
this.playground,
this.playground.width / 2 / this.playground.scale,
0.5,
0.05,
"white",
0.15,
"enemy",
username,
photo,
);
//每一个对象的uuid等于创建它窗口的uuid
player.uuid = uuid;
this.playground.players.push(player);
}
}
其他具体修改见gitee
提交implement multiplayer section-1
存在bug
,虽然会把房间人数限制在3,但是可能会成为4。