Unity 3D 大学物理虚拟试验平台后端搭建

此文档即为 Halluce 0.1 版本。

0. 需求分析与前置工作

与后端有关的项目需求:

  • 前端使用 Unity 3D 与 C# 代码,便于导入 3dMax 中的模型;

  • 使用 Python 代码实现相关算法,算法结果为文本/图片;

  • 项目可以通过 Web 访问,如浏览器输入网址方式可以直接查看。

解决方案:

  • Unity 支持 WebGL 导出方式,导出文件含 index.html ,可以将 WebGL 项目部署在远程服务器上;

  • C# 代码通过发 HTTP 请求的方式,调用Python后端代码,再通过Python代码进行计算,获取返回值;或者Python代码将图片结果生成至服务器某路径下,形成图床

  • C# 代码通过发 HTTP 请求的方式,获取在服务器内储存的生成图片(图床);

注意:Unity项目以WebGL形式导出时,所有C#代码最终会以JavaScript代码形式呈现,所以到最后还是JS代码发请求

技术栈使用:

  • 前端呈现:Unity2021.3.20f1c1 + C# + WebGL2

  • 后端与算法程序:Python 3.12

    • 算法框架:matpotlib 3.8.2

    • 后端框架:Flask 3.0.1

  • 服务器部署:阿里云ECS CentOS7.9 + Nginx 1.20.2 + 宝塔面板 BtPanel 免费版8.0.5

本文档需要以下技术基础:

  • Unity + C# 基本使用

  • Python3 基本语法

  • 云服务器的购买与配置 Linux命令基础

  • 宝塔面板BtPanel的安装以及宝塔面板站点的配置

1. WebGL 的导出与部署

1.1 将Unity项目更改为WebGL格式

打开Files -> Build Settings ,将 Platform 切换为 WebGL(点击Switch Platform) ,若本地不存在 WebGL 环境可以根据相应提示下载安装。

1.2 WebGL 格式配置

可以看出现在无法 Build And Run,这是因为我们还没有配置好WebGL。

我们首先点开左下角的 Player Settings... 这里列出几个比较核心的部分,其余部分不在此深究。

  • Other Settings - 取消勾选 Auto Graphics API ,选择WebGL 2;

  • Other Settings - C++ Compiler Configuration 改为 Debug:便于 Js控制台打印Console.log信息

这时 Ctrl+S 保存,可以看到Build and Run变为可以点击:

1.3 WebGL 项目导出

右键 Build And Run 选择一个合适的目录

  • 桌面下的一个英文文件夹即可

  • 不要将输出目录放在项目本体文件夹下

大约20s~5min后,浏览器自动弹出本地网页的窗口,WebGL项目导出成功:

打开导出的项目目录,可以看到以下文件:

1.4 WebGL 项目部署在服务器上

在宝塔面板上添加一个站点,相关配置参考如下:

将刚才导出的文件传到站点目录中

运行8085端口的项目。确保宝塔面板和阿里云放行端口后即可访问该端口,看到页面。

2. Unity 项目读取图片

Unity 项目前端一般使用RawImage组件来加载图片,后端逻辑目前有两种方式实现:

  1. RawImage组件读取项目的Assets/StreamingAssets/xxx.jpg加载图片,即C#代码中的 Application.StreamingAssetsPath

  2. 向后端Web图床(服务器内/公有云)发起Http请求,RawImage组件加载后端返回的图片

如果项目在本地运行,为了减少各种服务器的配置,我会采取第一种方案;但由于WebGL无法读取Assets/StreamingAssets目录(javascript代码在控制台报错),我们只能使用第二种方案——有点Web前后端的感觉。

以下我们用宝塔面板配置图床地址为例:

2.1 Nginx 请求配置

首先,打开编辑Nginx的配置文件(宝塔面板位于ngixn管理->配置修改);

我们只需要关注http.server模块,即服务器块。

当你配置 Nginx,你主要通过定义服务器块(server blocks)来指定不同虚拟主机的行为。每个服务器块对应一个虚拟主机,允许你在同一台服务器上托管多个域名或应用。以下是典型的 Nginx 服务器块的配置结构:

server {
    listen 80;  # 监听的端口
    server_name example.com;  # 域名
    root /path/to/your/site;  # 网站根目录

    # 配置项...
    location / {
        # 处理根路径请求的配置
    }

    location /images {
        # 处理 /images 路径请求的配置
    }

    # 其他配置...
}

server {
	listen 8080;
	# 其他配置...
}

让我们解释这个配置的各个部分:

  • server: 定义一个服务器块的开始,可以有多个服务器块。

  • listen: 指定服务器块监听的端口。例如,listen 80; 表示监听 HTTP 请求的默认端口。

  • server_name: 指定该虚拟主机对应的域名。多个域名可以用空格分隔,或者使用通配符。

  • root: 指定服务器块的根目录,即网站文件的基本路径。

  • location: 配置不同路径的请求处理。可以有多个 location 块,每个块可以有自己的配置项,例如 aliasproxy_pass 等。

本项目的参考配置代码:

server {
    listen 8085;  # 你的端口号
    server_name 47.93.19.130;  # 服务器的IP地址
    root /www/wwwroot/47.93.19.130:8085;  # Web 应用程序的根目录

    location / {

        try_files $uri $uri/ /index.html;  # 处理请求,优先尝试直接访问文件,然后尝试访问目录,最后返回index.html
    }

    location /image.jpg {
        alias /www/wwwroot/47.93.19.130:8085/StreamingAssets/sp1.jpg;  # 图片存储目录的绝对路径
        try_files $uri $uri/ /index.html;  
    }
}

此时访问 http://47.93.19.130:8085/image.jpg 便可以看到/下载图片:

2.2 C#代码发起请求

这里使用协程加载图片,`LoadButton` 方法需要关联到按钮代码中,`imgPath` 视情况修改:

void LoadButton(){
	private string imgPath = "http://47.93.19.130:8085/image.jpg";
	StartCoroutine(LoadRemoteImage(imgPath));
}

IEnumerator LoadRemoteImage(string url)
{
    using (UnityWebRequest www = UnityWebRequestTexture.GetTexture(url))
    {
        yield return www.SendWebRequest();

        if (www.result == UnityWebRequest.Result.Success)
        {
            // 获取下载的纹理
            Texture2D texture = ((DownloadHandlerTexture)www.downloadHandler).texture;
            showImage.texture = texture;
        }
        else
        {
            Debug.LogError($"Failed to load image: {www.error}");
        }
    }
}

在 Unity 中运行相关代码,可以看见成功读取了图片:

2.3 Nginx 跨域配置

然而,当我们将项目以WebGL形式导出,以本地形式运行时,点击按钮,却报出以下错误:

看到 CROS policy ,脑海里第一反应就是:跨域问题!

关于什么是跨域问题:查看链接

我们可以在 Nginx解决跨域问题,在 location /image.jpg 处添加以下代码:

location /image.jpg {
	# 图片存储目录的绝对路径
    alias /www/wwwroot/47.93.19.130:8085/StreamingAssets/sp1.jpg;  
	# 处理请求,优先尝试直接访问文件,然后尝试访问目录,最后返回index.html
    try_files $uri $uri/ /index.html;  

	# 解决跨域问题
    add_header 'Access-Control-Allow-Origin' '*';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}

再次点击按钮,不报相关错误,正确显示图片:

3. Flask 后端配置

3.1 Python 项目基本情况

该 Python 项目使用 matpotlib 框架,需要进行计算后输出结果。

3.2 Flask 后端程序

3.2.1 选型原因

我们后端程序接收到请求之后,要运行一个Python程序,因此选择 Python 语言的后端程序最方便。

而Python后端框架中,Django大而全,需要更多配置;Flask更轻量,导包后最少一个文件就可以做一个Controller接口,几乎不需要几行代码就可以实现。

因此,本项目使用Flask框架

3.2.2 程序示例

# 导Flask相关包
from flask import Flask
from flask_cors import CORS
# 导算法程序模块
import test
# 基础配置
app = Flask(__name__)
application = app
CORS(app)	# 跨域配置
location = r'/www/wwwroot/47.93.19.130:8085/StreamingAssets'

# 路由配置
@app.route('/<param1>/<param2>/<param3>', methods=['GET'])
def vision(param1, param2, param3):
    # 执行算法程序
    test.generate(param1, param2, param3, location)
    # 返回相关内容
    return "{\"success\": \"" + "true\", " + "\"data\": \"" + param1 + ' ' + param2 + ' ' + param3 +  ' ' + location + "\"}\n"

# 启动类
if __name__ == '__main__':
	# 关闭debug模式
    app.debug = False
	# 运行Flask程序,host字段表示所有ip均可访问,端口为8089
    app.run (host = '0.0.0.0', port = 8089)

3.3 Flask 项目配置

3.2.1 准备:服务器 Python 版本

Flask 要求服务器 Python 版本要在3.9以上,目前服务器内为3.6版本,我们需要手动再装一个:

方案一:使用 Anaconda (略)
方案二:使用宝塔面板

宝塔面板 -> 网站 -> Python项目 -> Python版本管理 ->安装合适的版本,这里安装了3.12.0:

路径一般位于:`/www/server/pyporject_evn/versions/3.12.0/bin/python3.12`

3.2.2 项目配置

方案一 使用 Pycharm专业版 远程开发

优点:这种方法只需要宝塔面板打开端口,然后剩下的全是PyCharm的任务,方便Python程序员,无需进行Git协作,导包方便

缺点:不在宝塔面板管理范围内,不方便控制项目

第一步:配置项目:

打开 Pycharm 2023.3.2 专业版 ,找到 Remote Development

选择 SSH Connection的New Connection,直接根据提示输入信息即可:

成功打开后,不要忘记切换Python版本:

然后就可以愉快开发了:

第二步:导包

用PyCharm 最方便的一点就是,可以在PyCharm里直接用图形界面导包(和在Windows上开发一样),不用再去宝塔面板/服务器命令搞事情,很大程度上方便了Python程序员的导包问题

第三步:开端口

这需要阿里云安全组+宝塔面板同时开端口

方案二 使用宝塔SSH + 宝塔Python项目配置

首先将文件上传到目录:

然后进入网站->Python项目->添加Python项目,注意框架、运行方式、网络协议

提交,运行!

3.4 Flask 跨域问题

请求Flask后端的跨域问题可以通过导包+一行代码解决:

from flask_cors import CORS
# 基础配置
app = Flask(__name__)
application = app
CORS(app)	# 跨域配置

3.5 C#代码发起请求

向之前请求图片一样,这里也发送请求,可以得到返回值(在本地的WebGL项目运行需要先对Flusk进行跨域处理)

    IEnumerator SendRequest()
    {
        string url = baseUrl + '/'+ lambda + '/' + radius + "/" +mode;
        Debug.Log(url);
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                string responseBody = request.downloadHandler.text;
                Debug.Log("Response: " + responseBody);

                // 在这里处理返回值,可以根据需要进行解析或其他操作

                // 继续其他操作...
            }
            else
            {
                Debug.LogError("Error: " + request.error);
            }
        }
    }

4. 目前的问题

  1. 程序发请求时,点击按钮后,加载图片响应不及时,必须多点击几次后才能正确加载。

解决方案:(1)延长加载时间,按按钮后增加程序的停止时间 (2)必须等待加载完图片后返回请求,必须等待请求返回后再发送加载图片的请求;(3)换一种加载图片的方法:接口本身直接返回图片,两个接口变成一个