网页地址:https://app6578.acapp.acwing.com.cn/
7.1 实现联机对战上
统一长度单位
在AcGamePlayground 类中
将this.root.$ac_game.append(this.$playground);
移到构造器的this.hide()
下面
编写resize()函数,使画布统一比例16:9 ,同一单位scale
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();
}
show() { // 打开playground界面
this.$playground.show();
this.resize();
修改game.cs使地图居中
width: 100%;
height: 100%;
user-select: none;
background-color: grey;
}
.ac-game-playground > canvas {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
在GameMap类里面,实时变化地图黑框大小,并去掉中间渐变的过程
resize() {
this.ctx.canvas.width = this.playground.width;
this.ctx.canvas.height = this.playground.height;
this.ctx.fillStyle = "rgba(0, 0, 0, 1)";
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
}
地图里面所有元素都要等比例变化,改成相对大小,地图里面所有元素改成scale的一个小数
在AcGamePlayground 类中
this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, "white", 0.15, true));
for (let i = 0; i < 5; i ++ ) {
this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, this.get_random_color(), 0.15, false));
}
在Player里面修改 所有元素改成scale的单位值,在render里面改回绝对值
this.eps = 0.01
if (this.is_me) {
this.add_listening_events();
} else {
let tx = Math.random() * this.playground.width / this.playground.scale;
let ty = Math.random() * this.playground.height / this.playground.scale;
this.move_to(tx, ty);
}
this.playground.game_map.$canvas.mousedown(function(e) {
const rect = outer.ctx.canvas.getBoundingClientRect();
if (e.which === 3) {
outer.move_to((e.clientX - rect.left) / outer.playground.scale, (e.clientY - rect.top) / outer.playground.scale);
} else if (e.which === 1) {
if (outer.cur_skill === "fireball") {
outer.shoot_fireball((e.clientX - rect.left) / outer.playground.scale, (e.clientY - rect.top) / outer.playground.scale);
}
shoot_fireball(tx, ty) {
let x = this.x, y = this.y
let radius = 0.01;
let angle = Math.atan2(ty - this.y, tx - this.x);
let vx = Math.cos(angle), vy = Math.sin(angle);
let color = "orange";
let speed = 0.5;
let move_length = 1;
new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, 0.01);
}
this.radius -= damage;
if (this.radius < this.eps) {
this.destroy();
return false;
}
update() {
this.update_move();
this.render();
}
update_move() { // 更新玩家移动
this.spent_time += this.timedelta / 1000;
if (!this.is_me && this.spent_time > 4 && Math.random() < 1 / 300.0) {
let player = this.playground.players[Math.floor(Math.random() * this.playground.players.length)];
this.shoot_fireball(tx, ty);
}
if (this.damage_speed > this.eps) {
this.vx = this.vy = 0;
this.move_length = 0;
this.x += this.damage_x * this.damage_speed * this.timedelta / 1000;
this.move_length = 0;
this.vx = this.vy = 0;
if (!this.is_me) {
let tx = Math.random() * this.playground.width / this.playground.scale;
let ty = Math.random() * this.playground.height / this.playground.scale;
this.move_to(tx, ty);
}
} else {
this.move_length -= moved;
}
}
}
render() {
let scale = this.playground.scale;
if (this.is_me) {
this.ctx.save();
this.ctx.beginPath();
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.stroke();
this.ctx.clip();
this.ctx.drawImage(this.img, (this.x - this.radius) * scale, (this.y - this.radius) * scale, this.radius * 2 * scale, this.radius * 2 * scale);
this.ctx.restore();
} else {
this.ctx.beginPath();
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
修正fireball, particle
// fireball
this.eps = 0.01;
render() {
let scale = this.playground.scale;
this.ctx.beginPath();
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
//particle
this.eps = 0.01;
render() {
let scale = this.playground.scale;
this.ctx.beginPath();
this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}
git 一下git commit -m "normalizing unit"
在static/js/src/menu/zbase.js里面outer.root.playground.show("single mode");
`outer.root.playground.show("multi mode");
进行区分
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();
});
}
修改一下角色分类:”me”, “robot”, player类改contructor里面加上username和photo
将this.resize();放到new GameMap(this)下面
实现联机对战
在acapp上开三个窗口相当于每个窗口都可以看到其他玩家,也就是相当于每开一个窗口要将用户信息上传到服务器,并且把当前用户信息发到前一个窗口(打开窗口>=2),每次当前窗口用户移动后,会把信息告诉server,然后server发送给其他窗口(广播),实现同时移动
要实现四个函数
1.create-player
2.move-to
3.shoot-fireball
4.attack(不同窗口可能存在延迟,所以要用attack特判,判断有无击中都是在每个窗口判断,不是在服务器判断)
http单向通信->https,websocket双向通信->wss加密协议
配置channels_redis
用来通信
- 在项目地址下/acapp/安装channels_redis:
pip install channels_redis - 配置acapp/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_test.routing import websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'acapp_test.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})
- 配置acapp/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 = 'acapp_test.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
- 配置game/routing.py
这一部分的作用相当于http的urls。
内容如下:
from django.urls import path
websocket_urlpatterns = [
]
- 编写game/consumers
这一部分的作用相当于http的views。
在game_test/consumer/multiplayer里面touch __init__.py
vim index.py
参考示例:
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)
当我们在前端执行this.ws = new WebSocket("wss://app165.acapp.acwing.com.cn/wss/multiplayer/");
的时候,如果同意创建连接def connect(self)
就调用await self.accept()
函数,async def disconnect(self, close_code):
断开连接,receive(self, text_data):
前端向后端发请求
group的概念,可以把很多不同的连接放到一个组里channel_layer.group_add
,group_send可以实现群发消息
- 启动django_channels
在~/acapp目录下执行:
编写联机逻辑
前端在playground里面需要建立一个和服务器的连接mkdir socket
cd socket mkdir mutiplayer
进入mutiplayer里面vim zbase.js
建立连接命令:this.ws = new WebSocket("wss://app6875xxxx")
class MultiPlayerSocket {
constructor(playground) {
this.playground = playground;
this.ws = new WebSocket("wss://app165.acapp.acwing.com.cn/wss/multiplayer/");
this.start();
}
start() {}
}
写一下路由,routing.py
from django.urls import path
from game.consumers.multiplayer.index import MultiPlayer
websocket_urlpatterns = [
path("wss/multiplayer/", MultiPlayer.as_asgi(), name="wss_multiplayer"),
]
vim playground/zbase.js
在”multi mode”里面this.mps = new MultiPlayerSocket(this);
else if (mode === "multi mode") {
this.mps = new MultiPlayerSocket(this);
this.mps.ws.onopen = function() {//链接创建成功的时候会回调的函数 调用一个创建玩家的消息
outer.mps.send_create_player();
};
}
}
在class MultiPlayerSocket 里面实现一个创建玩家的消息,需要两个函数一个向后台发送创建玩家的消事件,一个后端向前端接收玩家的事件
send_create_player(username, photo) {
this.ws.send(JSON.stringify({
'message': "hello app",
}));
}
receive_create_player(uuid, username, photo) {
}
编写同步函数,需要player,fireball等AcGameObject类里面加一个唯一编号,可以知道去同步哪个物品
this.timedelta = 0; // 当前帧距离上一帧的时间间隔
this.uuid = this.create_uuid();
}
create_uuid() {
let res = "";
for (let i = 0; i < 8; i ++ ) {
let x = parseInt(Math.floor(Math.random() * 10)); // 返回[0, 1)之间的数
res += x;
}
return res;
}
每个窗口就用当前玩家的uid
因为信息是广播传送的,所以要判断自己发的广播不需要自己重复创建玩家
else if (mode === "multi mode") {
this.mps = new MultiPlayerSocket(this);
this.mps.uuid = this.players[0].uuid;//第一个加入的玩家就是自己
this.mps.ws.onopen = function() {//链接创建成功的时候会回调的函数 调用一个创建玩家的消息
outer.mps.send_create_player();
};
}
}
//class MultiPlayerSocket
send_create_player() {
let outer = this;
this.ws.send(JSON.stringify({
'event': "create_player",
'uuid': outer.uuid,
}));
}
receive_create_player(uuid, username, photo) {
}
将n个玩家同步到一个窗口里,同步create_player事件,需要在服务器端存每局对战的所有信息,可以存到redis里面:room_0–第一局room_1–第二局
每一局的人数上限放到settings.py里面
ROOM_CAPACITY = 3
game/consumers/multiplayer/index.py,最多100个房间,服务器向本地发送当前已有信息
from django.conf import settings
from django.core.cache import cache
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) # 有效期1小时
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)
else if (mode === "multi mode") {
this.mps = new MultiPlayerSocket(this);
this.mps.uuid = this.players[0].uuid;
this.mps.ws.onopen = function() {
outer.mps.send_create_player(outer.root.settings.username, outer.root.settings.photo);
};
}
//class MultiPlayerSocket
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) {
}
this.ws.send向后台发送请求,receive函数判断event来做个路由
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) # 有效期1小时
await self.channel_layer.group_send( #群发消息给组内所有人
self.room_name,
{
'type': "group_create_player",
'event': "create_player",
'uuid': data['uuid'],
'username': data['username'],
'photo': data['photo'],
}
)
async def group_create_player(self, data): #接收的名字就是type关键字
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)
在前端接收群发信息,如果是自己就pass
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);
}
};
}
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,
);
player.uuid = uuid;//等于创建窗口的uid
this.playground.players.push(player);
}
注意点
记录一下这里的坑点,项目进行到这里已经需要多个服务了,顺序是:
启动nginx服务sudo /etc/init.d/nginx start
启动redis-server服务 sudo redis-server /etc/redis/redis.conf
启动uwsgi服务 uwsgi –ini scripts/uwsgi.ini
启动 django_channels服务 daphne -b 0.0.0.0 -p 5015 acapp.asgi:application
create player流程:
1.前端,当用户点击menu页面的multi mode时,menu/zbase.js 中的函数被调用,menu.hide(),playground.show(multi mode),在playground/zbase.js 中的show函数被调用,创建mps(multiplayersocket)对象,添加监听函数onopen(链接创建成功时,会被自动调用)。
2.后端,尝试链接时,consumers/multiplayer/index.py 中的connect函数被系统自动调用,先得到一个合法房间,如果链接成功,将房间中已存在的用户,加入到新用户的游戏页面中,也就是发送给新玩家的前端。
3.前端,在上一步中的链接创建成功时,onopen被自动调用,socket/multiplayer/zbase.js 中的send_create_player函数被调用,向后端发送新玩家信息和event类型。
4.后端,consumers/multiplayer/index.py 中的receive函数被系统自动调用,得到event类型和玩家信息并判断,调用create_player函数,找到对应房间,并将新玩家添加进去,同时群发通知更新,最后通过group_create_player将数据发送给每个玩家的前端。
当 前端 收到信息时,socket/multiplayer/zbase.js 中的receive函数中的监听函数onmessage被自动调用,接收到玩家数据和event类型,经判断如果不是自己(uuid不同)后,根据数据在playground中添加新的player对象,并在网页中显示出来。