Django上课笔记(一)——环境配置与项目创建(过程十分详细) - AcWing
(更新版)Django上课笔记(二)——菜单模块的实现, 含自动创建项目的脚本
Django上课笔记(三)——简单游戏的实现(模块拆分化详解) - AcWing
Django上课笔记(四)——(用户系统的实现) - AcWing
pycharm连接服务器同步写代码(图文详细过程)
linux基础课thrift详细开发过程 - AcWing
https://git.acwing.com/codeRokie/acapp
原因:
长度
概念16:9
game/static/js/src/playground/zbase.js
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();
}
我们看到,由于this.scale = this.height;
实际上地图的高度被设置为单位1
受此影响,每个玩家在被创建时传递的参数为:
new Player(this, this.width / 2 / this.scale, 0.5, 0.05, "white", 0.15, "me", this.root.settings.username, this.root.settings.photo);
仔细观察,画面中心高度的坐标为0,5
,所有坐标的参考系都以单位1
为基准
因此我们需要在game.js
中全局搜索关键字:
this.playground.height
:将其改为1
this.playground.width
:将其改为this.playground.width / this.playground.scale
同时,自己实现的特性的参数,也要酌情修改
监听浏览器窗口大小变化的事件,每当有此事件发生,就调用resize()
game/static/js/src/playground/zbase.js
add_listening_events() {
let outer = this;
//$(window).resize()在浏览器窗口大小改变时调用
$(window).resize(function () {
outer.resize();
});
}
并在start()
中调用
1.在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();
});
}
2.在game/static/js/src/playground/zbase.js
中,根据用户选择展示对应菜单
/**
* 根据模式打开对应界面
* @param mode
*/
show(mode) {
// 打开playground界面
let outer = this;
this.$playground.show();
this.root.$ac_game.append(this.$playground);
this.width = this.$playground.width();
this.height = this.$playground.height();
//创建GameMap对象
this.game_map = new GameMap(this);
this.resize();
this.create_player();
}
django_channels
django_channels官网
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.routing import websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'acapp.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.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
game/routing.py
这一部分的作用相当于http
的urls
内容如下:
在game
下创建routing.py
from django.urls import path
websocket_urlpatterns = [
]
game/consumers
这一部分的作用相当于http
的views
。
在game/consumers/mutiplayer/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)
在~/acapp
目录下执行:
daphne -b 0.0.0.0 -p 5015 acapp.asgi:application
详细请看:
基本的互联网通信协议都有在RFC文件内详细说明: websocket规范 RFC6455中文版
高质量博客:谈谈Websocket ,HTTP/TCP
1.TCP协议对应于传输层,而HTTP和websocket协议对应于应用层;HTTP和websocket都建立在TCP之上
2.Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的。
3.Http连接是一种短连接,是一种无状态的连接。所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。
4.用http协议想实现双向通信的方法是轮询
和长轮询
,这两种方法有两大弊端:
5.WebSocket是HTTP协议的拓展,80和443端口可以同时支持WebSocket和HTTP,它必须依赖 HTTP 协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
6.一旦客户端和服务器都发送了他们的握手,如果握手成功,传输数据部分开始。这是一个双向传输通道,每个端都能独立、随意发送数据。且是一种长连接
7.在TCP上实现帧机制,来回到IP包机制,而没有长度限制。比http的请求头要小的多
acapp/wsgi.py
和acapp/asgi.py
整合django_channels
,用websocket
实现:
1.每个物体的位置,通过向服务器发送move_to()
函数及其参数
2.每个玩家的指令
,包括释放各种技能,发送shoot_fireball()
函数
由于实际中不同客户端的网速和性能差异,在判断是否击中了谁
和同一时刻,每个物体在客户端上的真实位置
时,会出现判断混乱
所以,统一把事件的决定权交给释放技能且命中的玩家
。只要在某客户端有技能命中,不管其他人的状态,,之和服务器发送xxx被击中
的事件
。一旦判断你有被击中的状态
,不管在客户端情况为何,都会强制更新你的状态
注意:与主机通信的单位不是物体
而是房间
还是经典的三大块:前端
、路由
、业务控制层(consumers)
在game/consumers/multiplayer/index.py
中实现MultiPlayer
类。MultiPlayer
类的实体即为主机。
MultiPlayer
类继承自AsyncWebsocketConsumer
类
AsyncWebsocketConsumer
类的基本框架(模板):
class EchoConsumer(AsyncConsumer):
async def connect(self, event):
async def receive(self, event):
async def disconnect(self, close_code):
模板中固定要实现的三个函数:
connect
: 建立连接后执行的函数disconnect
:断开连接时执行的函数‘receive
: 主机在接收到客户端消息后调用的函数主机只有在接收到消息后才会广播(即调用不同业务的send函数)
在game/routing.py
中
from django.urls import path
from game.consumers.mutiplayer.index import MultiPlayer
websocket_urlpatterns = [
path("wss/mutiplayer/" ,MultiPlayer.as_asgi,name = "wss_multiplayer"),
]
前端需要实现一个MultiPlayerSocket
类,去与主机连接,并实现一系列数据的发送,以及接收主机数据,并对每种作出一系列相应处理
在game/static/js/src/playground/socket/multiplayer/zbase.js
中
class MultiPlayerSocket {
constructor(playground) {
this.playground = playground;
//建立websocket连接
this.ws = new WebSocket("wss://app220.acapp.acwing.com.cn/wss/multiplayer/");
this.uuid = null;
this.start();
}
start() {
this.receive();
}
/**
* 通过每个物体的唯一id去找到对应的对象
* @param uuid
* @returns {null|*}
*/
get_player(uuid) {
let players = this.playground.players;
for (let i = 0; i < players.length; i++) {
if (players[i].uuid === uuid) {
return players[i]
}
}
return null
}
/**
* 接收主机发来请求,并控制实现各种业务逻辑
*/
receive() {
}
主机只有在接收到消息后才会在不同的模块中调用不同业务的send函数
我们一共需要同步3类函数
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):
print("连接成功")
await self.accept()
# 主机与客户端断开连接时的函数
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.room_name, self.channel_name)
# 处理主机接收到的消息的函数
async def receive(self, text_data):
data = json.loads(text_data)
event = data['event']
# 每个事件交给不同函数处理
if event == "create_player":
await self.create_player(data)
elif event == "move_to":
await self.move_to(data)
elif event == "shoot_fireball":
await self.shoot_fireball(data)
elif event == "attack":
await self.attack(data)
elif event == "blink":
await self.blink(data)
async def group_send_event(self, data):
await self.send(text_data=json.dumps(data))
async def create_player(self, data):
self.room_name = None
# 遍历所有房间,房间上限暂定为1000
for i in range(100000000):
name = "room-%d" % (i)
# 如果redis中之前没有这个房间,且这个房间未满3人
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
if not cache.has_key(self.room_name):
# 在redis中创建一条房间数据{"房间号":[玩家uuid列表]}
cache.set(self.room_name, [], 3600) # 有效期1小时
# 官网对组的详解:https://channels.readthedocs.io/en/stable/topics/channel_layers.html#groups
# 将玩家以房间号分组
# 遍历当前房间中的所有玩家
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)
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为处理这个消息的函数名,是默认必须写的
'type': "group_send_event",
# 以下为自定义发送的消息
'event': "create_player",
'uuid': data['uuid'],
'username': data['username'],
'photo': data['photo'],
}
)
# 模板来源于官网:https://channels.readthedocs.io/en/stable/topics/consumers.html#websocketconsumer
小提示:在增加新技能时,需要在这里补充相应的函数
后端发送 | 前端发送 | 前端接收 | |
---|---|---|---|
移动 | async def move_to(self, data): await self.channel_layer.group_send( self.room_name, { # type为处理这个消息的函数名,是默认必须写的 ‘type’: “group_send_event”, # 以下为自定义发送的消息 ‘event’: “move_to”, ‘uuid’: data[‘uuid’], ‘tx’: data[‘tx’], ‘ty’: data[‘ty’], } ) |
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) { player.move_to(tx, ty); } } |
发射火球 | async def shoot_fireball(self, data): await self.channel_layer.group_send( self.room_name, { # type为处理这个消息的函数名,是默认必须写的 ‘type’: “group_send_event”, # 以下为自定义发送的消息 ‘event’: “shoot_fireball”, ‘uuid’: data[‘uuid’], ‘tx’: data[‘tx’], ‘ty’: data[‘ty’], “ball_uuid”: data[‘ball_uuid’], } ) |
send_shoot_fireball(tx, ty, ball_uuid) { let outer = this; this.ws.send(JSON.stringify({ ‘event’: “shoot_fireball”, ‘uuid’: outer.uuid, ‘tx’: tx, ‘ty’: ty, ‘ball_uuid’: ball_uuid, })); } |
receive_shoot_fireball(uuid, tx, ty, ball_uuid) { let attacker = this.get_player(uuid); if (attacker) { let fireball = attacker.shoot_fireball(tx, ty) fireball.uuid = ball_uuid; } } |
受到攻击 | async def attack(self, data): await self.channel_layer.group_send( self.room_name, { # type为处理这个消息的函数名,是默认必须写的 ‘type’: “group_send_event”, # 以下为自定义发送的消息 ‘event’: “shoot_fireball”, ‘uuid’: data[‘uuid’], ‘tx’: data[‘tx’], ‘ty’: data[‘ty’], ‘attacked_uuid’: data[‘attacked_uuid’], ‘angle’: data[‘angle’], ‘damage’: data[‘damage’], ‘ball_uuid’: data[‘ball_uuid’], } ) |
send_attack(attacked_uuid, x, y, angle, damage, ball_uuid) { let outer = this; this.ws.send(JSON.stringify({ ‘event’: “attack”, ‘uuid’: outer.uuid, ‘attacked_uuid’: attacked_uuid, ‘x’: x, ‘y’: y, ‘angle’: angle, ‘damage’: damage, ‘ball_uuid’: ball_uuid, })); } |
receive_attack(uuid, attacked_uuid, x, y, angle, damage, ball_uuid) { let attacker = this.get_player(uuid); let attacked = this.get_player(attacked_uuid); if (attacker && attacked) { attacked.receive_attack(x, y, angle, damage, “fireball”, ball_uuid, attacker); } } |
闪现 | async def blink(self, data): await self.channel_layer.group_send( self.room_name, { ‘type’: “group_send_event”, ‘event’: “blink”, ‘uuid’: data[‘uuid’], ‘tx’: data[‘tx’], ‘ty’: data[‘ty’], } ) |
send_blink(tx, ty) { let outer = this; this.ws.send(JSON.stringify({ ‘event’: “blink”, ‘uuid’: outer.uuid, ‘tx’: tx, ‘ty’: ty, })); } |
receive_blink(uuid, tx, ty) { let player = this.get_player(uuid); if (player) { player.blink(tx, ty); } } |
br/> ‘type’: “group_send_event”,
‘event’: “blink”,
‘uuid’: data[‘uuid’],
‘tx’: data[‘tx’],
‘ty’: data[‘ty’],
}
) | send_blink(tx, ty) {
let outer = this;
this.ws.send(JSON.stringify({
‘event’: “blink”,
‘uuid’: outer.uuid,
‘tx’: tx,
‘ty’: ty,
}));
} | receive_blink(uuid, tx, ty) {
let player = this.get_player(uuid);
if (player) {
player.blink(tx, ty);
}
} |