Django框架课-多人联机对战 (一)

Django框架课-多人联机对战(一)

统一度量单位

image

比如这两个地图,有的玩家是第一种,有的玩家的屏幕是第二种。
第一种地图的玩家移动到地图的左部分,在第二种地图里就可能已经出界了。
我们要在联机对战中统一所有地图的长宽比。不同玩家打开地图的长宽比应该是一样的都。

屏幕的长宽比一般是16:9

固定地图尺寸16:9

对于地图,首先要定义一个单位长度unit,用来表示一个unit有多长或多宽
但是对于长宽比并不是16:9的窗口

image
好比这窗口,不管他长宽比是多少,绝对不是16:9咯,假设里面红色的框框是16:9(应该不会差不了多少的)。那么限制这个红框的就是这个窗口的高度,如果再高一点,那么他还能在16:9时再宽一点,更大一点。

现在看来,宽高哪个小,就要按照哪个去计算unit,于是可以像下面这样去计算,宽/16,高/9取min就是unit的值

定义了unit,宽就是unit * 16,高就是unit * 9

有了unit,还要有一个scale,用来以后计算,画出各个物体之间的距离等,这里直接把scale的值取为height的值。

playground/zbase.js:

class AcGamePlayground{
	constructor(root){
		..
		this.root.$ac_game.append(this.$playground);
		..
	}
}

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() {
	..
	this.resize();
	..
}

playground/game_map/zbase.js

resize() {
        this.ctx.canvas.width = this.playground.width;
        this.ctx.canvas.height = this.playground.height;
        this.ctx.fillStyle = "rgba(0, 0, 0, 1)"; // 保证每次刷新不让他渐变成background-color,而是直接刷新成这个颜色(透明度变成了1)
        this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    }

css居中canvas:

.ac-game-playground > canvas {
    position: relative;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

调整窗口大小的时候使得地图等比例变化

调窗口大小,某个元素可能看不到了就。

image

比如这样子,我把窗口拉成这样,一些元素就看不到了,这个时候要把所有的数值改成相对大小就好了。

地图里的角色有:particle,player,skill(粒子,玩家,火球技能),只要改变这三个就可以了,将所有的度量单位改成相对值
包括在初始的时候,将所有的度量单位都改成相对值(除以一个scale)。

不过在最后使用canvas画的时候再将相对值改成绝对值去画(乘一个scale),这时画的时候是画相对于背景窗口大小的绝对坐标

注意什么需要改,什么不需要改。

玩家 Player
初始化的时候,转为传递 scale 的比例值

js/src/playground/zbase.js

class AcGamePlayground {
    ...
    show() {    //打开 playground 界面
        ...
        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));
        }

    }
}

js/src/playground/player/zbase.js

class Player {
    ...
    start() {
        if (this.is_me) {
            ...
        } else {
            let tx = Math.random() * this.playground.width / this.playground.scale;
            let ty = Math.random() * this.playground.height / this.playground.scale;
            ...
        }
    }
    add_listening_events() {
        ...
        this.playground.game_map.$canvas.mousedown(function(e) {
            ...
            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.0;
        let damage = 0.01;
        new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, damage);
    }
    ...
    update() {
        this.update_move();
        this.render();
    }
    update_move() { //更新玩家移动
        ...
        if (!this.is_me && this.spent_time > 4 && Math.random() * 180 < 1) {
            ...
        }
        if (this.damage_speed > this.eps) {
            ...
        } else {
            if (this.move_length < this.eps) {
                ...
                if (!this.is_me) {
                    let tx = Math.random() * this.playground.width / this.playground.scale;
                    let ty = Math.random() * this.playground.height / this.playground.scale;
                    ...
                }
            } else {
                ...
            }
        }
    }
    render() {
        let scale = this.playground.scale;
        if (this.is_me) {
            ...
            this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, Math.PI * 2, false);
            ...
            this.ctx.drawImage(this.img, (this.x - this.radius) * scale, (this.y - this.radius) * scale, this.radius * 2 * scale, this.radius * 2 * scale);
            ...
        } else {
            ...
            this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, 2 * Math.PI, false);
            ...
        }
    }
}

火球 Fireball
js/src/playground/skill/fireball/zbase.js

class Fireball {
    ...
    render() {
        let scale = this.playground.scale;
        this.ctx.beginPath();
        this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, 2 * Math.PI, false);
        this.ctx.fillStyle = this.color;
        this.ctx.fill();
    }
}

粒子 particle
js/src/playground/particle/zbase.js

class Particle {
    ...
    render() {
        let scale = this.playground.scale;
        this.ctx.beginPath();
        this.ctx.arc(this.x * scale, this.y * scale, this.radius * scale, 0, 2 * Math.PI, false);
        this.ctx.fillStyle = this.color;
        this.ctx.fill();
    }
}

重新git clone之后502了

bug: 粒子效果消失,窗口变化游戏地图没有等比例变化,刚开始碰到了这个bug

不知道为什么,playground/zbase.js的构造函数好像没有执行,或者是start()未执行

找了好久没找到,然后将项目恢复到上次实现acpp端acwing一键登录的现场之后,直接502错误了
image

找到错误原因了,服务没开启,scripts下的文件
image
文件并没被删除,但是需要重新输入一下启动服务的代码

原因是这样的:我将整个acapp文件删除后,重新clone过来项目,和原来的路径并不一样的,而我tmux中的路径还是原来上次删除之前的路径qwq

重新写后没问题了

增加联机对战模式

修改is_me为character

两种游戏模式:单机(single mode),联机(multi mode)

先将两种模式在menu菜单初始化的时候传一个参数用以区分玩家是选择了哪个模式。然后以后渲染页面是否渲染人机。

有三种角色:人机(robot),玩家自己(me),其他玩家(enemy)

给player添加一个新的属性character,如果值是"robot"说明是人机,值为"me"说明是玩家自己,值为"enemy"说明是其他玩家

然后将所有文件的传参改成character

menu/zbase.js

class AcGameMenu{
    ...
    add_listening_events() {
        ...
        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");
        });
        ...
    }
    ...
}

playground/zbase.js

class Playground {
    ...
    show(mode) {    //打开 playground 界面
        ...
        this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, "white", 0.15, "me", this.root.settings.username, this.root.settings.photo)));
        if (mode === "single mode") {
            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, "robot"));
            }
        } else if (mode === "multi mode") {
        }

    }
    ...
}

playground/player/zbase.js

class Player extends AcGameObject {
    constructor(playground, x, y, radius, color, speed, character, username, photo) {
        ...
        this.character = character;
        this.username = username;
        this.photo = photo;
        ...
        if (this.character !== "robot") {
            this.img = new Image();
            this.img.src = this.photo;
        }
    }
    ...
    //同理,根据对应的逻辑,修改后面所有的 is_me 为 character
}

联机对战原理

需要实现四个事件的同步:

1.create player
2.move to
3.shoot fireball
4.attack

image

比如添加玩家这个事件同步是这样的:
刚开始这个地图里只有1号和2号玩家。3号玩家想要加入到这个对战,那么3号玩家向服务器发送请求,告诉服务器自己要加进去这个对战。
服务器接收到3号玩家的请求之后,向已经在地图中的1号2号两名玩发送信息,告诉1号和2号,3号要加进来了。服务器将1号2号及其信息状态加入到3号的地图里,将3号及其信息和状态加入到1号和2号的地图里。
然后就实现了同步。

可以发现是存在延迟的,一般的fps游戏是否攻击到某名玩家都是在客户端本地判断,不是在服务器端判断。缺点是可以作弊,安全性不强,好处是延迟会小,玩家体验会好一些。比如在我们自己的窗口已经真的击中了,而服务器还在判断是否击中,这样就会有延迟很影响体验感。

比如3号向某个敌人发射了一个火球,是3号的窗口去判断(3号玩家本地判断)有没有击中,如果击中了,就告诉服务器,服务器告诉1号和2号说谁被击中了。

websocket协议

想要实现这四个事件的同步,我们需要一台中心服务器,这个中心服务器要求可以双向通信。之前实现的功能都是使用的http协议,是一种单向协议。

需要使用ws协议(websocket协议),是一个双向协议,支持双向通信。

http对应的加密协议是https协议,ws协议也有其加密协议->websockets协议(wss协议)。

django有一个让django支持wss协议的django_channels。

补充:

Django Channels 是一个用于构建实时 Web 应用程序和网络应用程序的 Django 扩展库。它允许 Django 开发人员使用 WebSocket,HTTP/2 和 ASGI(异步服务器网关接口)等协议来构建具有实时交互性的应用程序

WebSocket 是一种网络通信协议,它在客户端和服务器之间建立双向通信通道,使得客户端和服务器可以进行实时、持久性的数据交换。
WebSocket 协议通过在客户端和服务器之间建立一条长连接,使得服务器可以主动向客户端推送数据,而不需要等待客户端的请求。这种双向通信的方式可以大大提高 Web 应用程序的实时性和交互性,为实时聊天、实时数据更新、在线游戏等应用场景提供了便利。

wss 是 WebSocket Secure 的简称,它是 WebSocket 协议的一种加密协议,使用了 SSL/TLS 加密通信,可以在 WebSocket 连接中提供安全性。

配置channels_redis

channels_redis简介

Django Channels 和 channels_redis 来处理 WebSocket 连接,可以实现双向通信.

配置

参考链接:https://www.acwing.com/blog/content/12692/

  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))
})
  1. 配置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)],
        },
    },
}
  1. 配置game/routing.py
    这一部分的作用相当于http的urls。
    内容如下:
from django.urls import path

websocket_urlpatterns = [
]
  1. 编写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)
  1. 启动django_channels
    在~/acapp目录下执行:
daphne -b 0.0.0.0 -p 5015 acapp.asgi:application

此时已经将wss协议的服务端启动起来了。

此时启动了什么服务都

记录一下这里的坑点,项目进行到这里已经需要多个服务了,顺序是:

启动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

写前端js

窗口和服务器是双向通信的,需要从窗口向服务器通过socket建立连接一个连接

建立连接后,向后端发送一个create player的请求,需要实现两个函数,第一个函数实现向后端发送请求。当玩家进入地图需要告诉服务器通知其他玩家的终端。
第二个函数实现从后端接受这个请求。当其他玩家进入地图的时候,服务器向自己发来的请求。

playground/zbase.js

class AcGamePlayground {
    ...
    show(mode) {    //打开 playground 界面
        ...
        if (mode === "single mode") {
            ...
        } else if (mode === "multi mode") {
            this.mps = new MultiPlayerSocket(this);
            this.mps.ws.onopen = function() {
                outer.mps.send_create_player();
            };
        }

    }
}

playground/socket/multiplayer/zbase.js

class MultiPlayerSocket {
    constructor(playground) {
        this.playground = playground;
        this.ws = new WebSocket("wss://app1117.acapp.acwing.com.cn/wss/multiplayer/");
        this.start();
    }
    start() {
    }
    send_create_player() {
        this.ws.send(JSON.stringify({
            'message': 'hello acapp server',
        }));
    }
    receive_create_player() {
    }
}
posted @ 2023-03-06 19:32  r涤生  阅读(135)  评论(0编辑  收藏  举报