Django项目笔记——联机对战的实现

Posted 卷王2048

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Django项目笔记——联机对战的实现相关的知识,希望对你有一定的参考价值。

Django上课笔记(五)——联机对战的实现

也欢迎大家光临我另外项目课的其他博客:

Django上课笔记(一)——环境配置与项目创建(过程十分详细) - AcWing

(更新版)Django上课笔记(二)——菜单模块的实现, 含自动创建项目的脚本

Django上课笔记(三)——简单游戏的实现(模块拆分化详解) - AcWing

Django上课笔记(四)——(用户系统的实现) - AcWing

pycharm连接服务器同步写代码(图文详细过程)

linux基础课thrift详细开发过程 - AcWing

项目地址

https://git.acwing.com/codeRokie/acapp

前端度量标准的统一

原因:

  • 由于要涉及到多人联机。
  • 在客户端每个玩家的窗口大小可能不同,
  • 对应的对象坐标,地图大小可能各不相同,
  • 就无法实现多名玩家间的相互通信,为此,一定要统一在各种客户端情况下的长度概念

统一地图比例

1.原因:

  • 考虑到客户端的各种设备不统一
  • 浏览器窗口大小可以被用户调整为任意比例
  • 方便定义度量单位

2.思路:

  • 统一游戏地图比例为16:9
  • 以高度作为单位1
  • 若浏览器窗口比例不符,则按照窗口长宽中的较小者作为地图的长
  • 地图之外的部分可以做一些填充

3.实现:

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

同时,自己实现的特性的参数,也要酌情修改

使地图随着窗口大小变化

1.思路:

监听浏览器窗口大小变化的事件,每当有此事件发生,就调用resize()

2.实现

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官网

1.安装channels_redis

pip install channels_redis

2.配置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))
)

3.配置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)],
        ,
    ,

4.配置game/routing.py

这一部分的作用相当于httpurls
内容如下:

game下创建routing.py

from django.urls import path

websocket_urlpatterns = [
]

5.编写game/consumers

这一部分的作用相当于httpviews

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)

6.启动django_channels

~/acapp目录下执行:

daphne -b 0.0.0.0 -p 5015 acapp.asgi:application

联机对战架构

背景知识

websocket

详细请看:

基本的互联网通信协议都有在RFC文件内详细说明: websocket规范 RFC6455中文版

高质量博客:谈谈Websocket HTTP/TCP

观后总结(博主结合计网知识和网上的多篇博客和RFC)

1.TCP协议对应于传输层,而HTTP和websocket协议对应于应用层;HTTP和websocket都建立在TCP之上

2.Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的。

3.Http连接是一种短连接,是一种无状态的连接。所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。

4.用http协议想实现双向通信的方法是轮询长轮询,这两种方法有两大弊端:

  • 例如假设服务器端的数据更新速度很快,服务器在传送一个数据包给客户端后必须等待客户端的下一个Get请求到来,才能传递第二个更新的数据包给客户端,如果在网络拥塞的情况下,这个时间用户是不能接受的
  • 由于http数据包的头部数据量往往很大(通常有400多个字节),但是真正被服务器需要的数据却很少(有时只有10个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费。

5.WebSocket是HTTP协议的拓展,80和443端口可以同时支持WebSocket和HTTP,它必须依赖 HTTP 协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。

6.一旦客户端和服务器都发送了他们的握手,如果握手成功,传输数据部分开始。这是一个双向传输通道,每个端都能独立、随意发送数据。且是一种长连接

7.在TCP上实现帧机制,来回到IP包机制,而没有长度限制。比http的请求头要小的多

关于acapp/wsgi.pyacapp/asgi.py

思路

如何同步玩家信息

整合django_channels,用websocket实现:

  • 每个客户端需要给主机实时发送请求,"主机"的建立会在后面详解
  • 主机也需要实时向每个客户端发送广播。
  • 客户端在收到广播后要更新自己的信息

需要同步哪些信息

1.每个物体的位置,通过向服务器发送move_to()函数及其参数

2.每个玩家的指令,包括释放各种技能,发送shoot_fireball()函数

最终决定权的归属

由于实际中不同客户端的网速和性能差异,在判断是否击中了谁同一时刻,每个物体在客户端上的真实位置时,会出现判断混乱

所以,统一把事件的决定权交给释放技能且命中的玩家。只要在某客户端有技能命中,不管其他人的状态,,之和服务器发送xxx被击中

的事件。一旦判断你有被击中的状态,不管在客户端情况为何,都会强制更新你的状态

主机、连接和客户端

注意:与主机通信的单位不是物体而是房间

前后端建立通信(架构部分)

还是经典的三大块:前端路由业务控制层(consumers)

后端业务控制层(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);

|

以上是关于Django项目笔记——联机对战的实现的主要内容,如果未能解决你的问题,请参考以下文章

[教你做小游戏] 用86行代码写一个联机五子棋WebSocket后端

双人对战的球类游戏ios源代码项目

Android 蓝牙对战五子棋项目实现(含人机对战功能)

Java五子棋课程设计

华为联机对战下载运行华为官方Unity示例代码,提示鉴权失败并返回错误码100114

对战的小游戏