6、联机对战上

源码地址https://gitee.com/pxlsdz/hyld

演示地址http://121.199.59.80/hyld/

统一长度单位

各个玩家的比要统一为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构造函数中再添加 usernamephoto

添加Django channels

思路

wss用来支持游戏同步以及聊天室功能

中心服务器与客户端需要双向连接,因此 http 协议(单线)不再适用,使用 websocket 协议(双向)。

http 的加密版是 httpsws 的加密版是 wss

Djangowss 的支持工具: Django Channels

同步4个事件

  1. create player通过 server 同步玩家信息,有几个玩家,各自的状态如何。(本文实现)
  2. move to同步移动,某个玩家的移动需要通知其他玩家,在其他玩家的窗口渲染
  3. shoot fireball火球发射,与同步玩家状态类似,同步火球的状态
  4. 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

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

from django.urls import path

websocket_urlpatterns = [
]

5. 编写hyld/game/consumers

这一部分的作用相当于httpviews

参考示例:

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 文件夹用来存储客户端创建的 websocketserver 端建立连接。

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.pywss的后端处理函数,有三个主要函数

  1. accept函数: 成功创建连接时被调用
  2. disconnect函数 :断开连接时调用
  3. 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中有apimps.ws.onopen() 会在连接创建成功时调用。

实现两个同步函数,从前端向服务端发送玩家创建的消息,另一个是服务端接收前端玩家创建的消息。

编写create player同步函数

所有的玩家和火球需要有唯一的编号,以便进行同步,在 gameobject中给每个游戏对象添加一个随机生成的8位的 uuid

哪个窗口创建了该对象,则 uuid 就使用哪个窗口的。

接收到自己发送的信息时应当忽略,因此需要判断信息是由谁发出的, wss发送信息时携带 playeruuid .

playground 中创建 web socket 对象 mps(multi player socket) 时, 将 mpsuuid 设置为 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。

posted @ 2021-12-27 21:20  pxlsdz  阅读(101)  评论(0编辑  收藏  举报