本文最终效果如下:
请添加图片描述
请添加图片描述

一、前言

嗨,大家好,我是新发。
我最近在找一些Unity面试题,然后我看到,有一些网站和小程序的答题需要钱,我比较穷,于是我决定自己做一个题库录入和刷题的程序,自给自足,方便自己整理题目,也顺便教一下大家,看看我是如何使用Unity制作Unity题库的。

二、方案设计

我想做的功能很简单,就是客户端录入题目,按题目分类存到服务端,客户端可以选择不同类别的题目进行随机刷题。
画个图:
在这里插入图片描述
客户端部分我使用Unity来做,服务端我准备使用python来写,使用tornadoWeb框架,题库数据库就使用简单的json文本好啦(因为题库的题目数量也不会特别巨量,只是纯粹地把数据落地到磁盘而已,不需要真正的数据库)。

三、界面设计

使用axure快速原型设计工具先简单设计一下界面,刷题界面如下:

在这里插入图片描述
试题录入界面如下:
在这里插入图片描述

四、UI素材获取

简单的UI素材资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮,

找一个形状合适的,可以进行调色,我一般是调成白色,

因为Unity中可以设置Color,这样我们只需要一个白色按钮就可以在Unity中创建不同颜色的按钮了。
弄点基础的美术资源,
在这里插入图片描述

五、Unity客户端部分

1、创建Unity工程

创建一个2D模板的Unity工程,工程名我定为UnityQuestionBank,如下:
在这里插入图片描述

注意:2D模板会去下载一些2D的工具,比如2D Sprite,所以创建工程需要稍微等一下,

2、分辨率设置

我想做成横版的,Game视图分辨率设置为1280 * 720
在这里插入图片描述
创建一个Canvas
在这里插入图片描述
Canvas组件的Render Mode设置为Screen Space - CameraRender Camera设置为主摄像机,
Canvas Scaler组件的UI Scale Mode设置为Scale With Screen SizeReference Resolution设置为1280 * 720,如下:
在这里插入图片描述

3、制作界面预设

根据界面设计,制作界面预设。

3.1、刷题界面:MainPanel.prefab

在这里插入图片描述
层级结构如下:
在这里插入图片描述

3.2、题目录入界面:QuestionInputPanel.prefab

在这里插入图片描述
层级结构如下:
在这里插入图片描述

3.3、提示语:FlyTips.prefab

再做一个提示语的预设,
在这里插入图片描述
层级结构如下,一个黑色背景图为父节点,文字为子节点,
在这里插入图片描述
提示语的背景需要根据文字自适应,
请添加图片描述
要实现上面的自适应新效果,只需在背景图挂Content Size FitterHorizontal Layout Group组件,
其中Content Size FitterHorizontal Fit设置为Preferred Size,因为我们只需要做横向自适应,
在这里插入图片描述
Horizontal Layout Group组件的Control Child Size勾选Width,这样文字子物体的宽度就可以控制背景图的宽度了,把Child Alignment设置为Middle Center,这样文字就居中对齐了,再设置一下PaddingLeftRight,让背景图的左右两侧留一些空白,
在这里插入图片描述
顺手给提示语预设做个动画,
请添加图片描述
动画文件记得把Loop Time勾选去掉,否则它会循环播放,
在这里插入图片描述

4、Http请求封装

Unity提供了一个UnityWebRequest类,可以很方便地执行Http请求。

注:关于UnityWebRequest的使用教程,我之前写过相关文章:《长江后浪推前浪,UnityWebRequest替代WWW》
《【游戏开发进阶】新发带你玩转Unity日志打印技巧(彩色日志 | 日志存储与上传 | 日志开关 | 日志双击溯源)》

4.1、封装HttpHelper类

我们封装一个HttpHelper类,因为请求结果需要想要有异步回调的功能,我们可以使用协程,要执行协程有两种方式,一种是在MonoBehaviour中调用StartCoroutine,另一种就是自己通过IEnumerator迭代器去MoveNext。这里我选择第一种方法,HttpHelper继承MonoBehaviour,调用StartCoroutine

// HttpHelper.cs

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System;

public class HttpHelper : MonoBehaviour
{
	// Web服务器地址
	public const string WebUrl = "http://localhost:7891/";
	
	 /// <summary>
    /// 请求题目所有类别
    /// </summary>
    public void StartGetAllQuestionTypes(Action<string> cb)
    {
		// StartCoroutine调用Get接口
    }

    /// <summary>
    /// 随机获取一个题目
    /// </summary>
    public void StartGetOneQuestion(string questionType, Action<string> cb)
    {
		// StartCoroutine调用Get接口
    }

    /// <summary>
    /// 试题录入
    /// </summary>
    /// <param name="questionType">题目类别</param>
    /// <param name="question">题目</param>
    /// <param name="code">代码</param>
    /// <param name="answer">答案</param>
    /// <param name="cb">回调</param>
    public void StartPostAddQuestion(string questionType, string question, string code, string answer, Action<string> cb)
    {
		// StartCoroutine调用Post接口
    }
}

接下来我们封装一下HttpGet接口和Post接口。

4.2、Http Get请求
// HttpHelper.cs 

// Http Get接口
IEnumerator CoroutineHttpGet(string url, Action<string> cb)
{
    UnityWebRequest req = UnityWebRequest.Get(url);
    yield return req.SendWebRequest();
    if (!string.IsNullOrEmpty(req.error))
    {
        Debug.Log(req.error);
        yield break;
    }
    cb?.Invoke(req.downloadHandler.text);
    req.Dispose();
}
4.3、Http Post请求
// HttpHelper.cs 

// Http Post接口
IEnumerator CoroutineHttpPost(string url, WWWForm form, Action<string> cb)
{
    UnityWebRequest req = UnityWebRequest.Post(url, form);
    yield return req.SendWebRequest();
    if (!string.IsNullOrEmpty(req.error))
    {
        Debug.Log(req.error);
        cb?.Invoke("{'error_code': -1}");
        yield break;
    }
    cb?.Invoke(req.downloadHandler.text);
    req.Dispose();
}
4.4、MonoBehaviour单例模式

另外,我想让HttpHelper全局只有一个实例,也就是单例模式,封装一个instance属性,

// HttpHelper.cs

// MonoBehaviour单例模式
private static HttpHelper s_instance;
   public static HttpHelper instance
   {
       get
       {
           if (null == s_instance)
           {
               var go = new GameObject("HttpHelper");
               s_instance = go.AddComponent<HttpHelper>();
           }
           return s_instance;
       }
   }
4.5、请求题目所有类别

我们把请求题目所有类别的接口加上Get调用,

// HttpHelper.cs

/// <summary>
/// 获取所有问题的类型
/// </summary>
public void StartGetAllQuestionTypes(Action<string> cb)
{
    StartCoroutine(CoroutineHttpGet(WebUrl + "get_question_types", cb));
}
4.6、随机获取一个题目

随机获取一个题目,需要告知服务器问题的类别,我们只需在请求链接尾部加上参数即可,例:

http://localhost:7891/get_one_question?question_type=C#基础

不过这里需要小心,因为URL只能使用英文字母、阿拉伯数字和某些标点符号,所以我们需要先对参数执行URL编码,UnityUnityWebRequest类中提供了URL编码的接口给我们:

public static string EscapeURL(string s);
public static string EscapeURL(string s, Encoding e);

对应的,URL解码接口:

public static string UnEscapeURL(string s);
public static string UnEscapeURL(string s, Encoding e);

最终,随机获取一个题目接口如下:

/// <summary>
/// 随机获取一个题目
/// </summary>
public void StartGetOneQuestion(string questionType, Action<string> cb)
{
	// 执行URL编码
    questionType = UnityWebRequest.EscapeURL(questionType);
    // 执行Get请求
    StartCoroutine(CoroutineHttpGet(WebUrl + "get_one_question?question_type=" + questionType, cb));
}
4.7、录入新题目

录入新题目,需要上传题目数据给服务器,我们要使用POST请求,数据要封装在WWWForm中,如下:

注:塞入WWWForm会自动处理成URL编码,所以不需要我们自己进行URL编码。

/// <summary>
/// 试题录入
/// </summary>
/// <param name="questionType">题目类别</param>
/// <param name="question">题目</param>
/// <param name="code">代码</param>
/// <param name="answer">答案</param>
/// <param name="cb">回调</param>
public void StartPostAddQuestion(string questionType, string question, string code, string answer, Action<string> cb)
{
	WWWForm form = new WWWForm();
	form.AddField("question_type", questionType);
	form.AddField("question", question);
	form.AddField("code", code);
	form.AddField("answer", answer);
	StartCoroutine(CoroutineHttpPost(WebUrl + "add_one_question", form, cb));
}
5、资源管理器:ResMgr.cs

接下来要做界面交互,在做界面交互之前,需要先能把界面显示出来,这里就涉及到界面资源的加载。
关于资源读取我之前写过相关文章:
《Unity游戏开发——新发教你做游戏(三):3种资源加载方式》

这里我就简单处理,通过Resources.Load来读取资源。
界面预设文件放在Resources目录中,如下:
在这里插入图片描述
然后我们封装一个资源管理器:ResMgr,逻辑很简单,通过Resources.Load加载资源,加载过的资源缓存到容器中,下次再调用则直接从缓存中取,
在这里插入图片描述

代码如下:

using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// 资源管理器
/// </summary>
public class ResMgr 
{
    public GameObject GetRes(string resPath)
    {
        if(m_prefabs.ContainsKey(resPath))
        {
            return m_prefabs[resPath];
        }
        var go = Resources.Load<GameObject>(resPath);
        m_prefabs[resPath] = go;
        return go;
    }

    private Dictionary<string, GameObject> m_prefabs = new Dictionary<string, GameObject>();

    private static ResMgr s_instance;
    public static ResMgr instance
    {
        get
        {
            if (null == s_instance)
                s_instance = new ResMgr();
            return s_instance;
        }
    }
}
6、UI管理器:UIMgr.cs

UI需要实例化,统一挂在Canvas节点下,所以我们这里再封装一个UI管理器,
在这里插入图片描述

代码如下:

using UnityEngine;

/// <summary>
/// UI管理器
/// </summary>
public class UIMgr
{
    public void Init()
    {
        m_canvasTrans = GameObject.Find("Canvas").transform;
    }

    public GameObject ShowUi(string resPath)
    {
        var prefab = ResMgr.instance.GetRes(resPath);
        if (null == prefab) return null;
        var uiObj = Object.Instantiate(prefab);
        uiObj.transform.SetParent(m_canvasTrans, false);
        return uiObj;
    }

    private Transform m_canvasTrans;

    private static UIMgr s_instance;
    public static UIMgr instance
    {
        get
        {

            if (null == s_instance)
                s_instance = new UIMgr();
            return s_instance;
        }
    }
}
7、刷题界面:MainPanel.cs

创建MainPanel.cs脚本,定义一些UI对象成员,

// MainPanel.cs

// 题目类别下拉框
public Dropdown questionTypeDropdown;
// 题目文本(含答案)
public Text questionText;
// 题目录入按钮
public Button inputQuestionBtn;
// 看答案按钮
public Button answerBtn;
// 下一题按钮
public Button nextBtn;

MainPanel.cs挂到MainPanel.prefab预设上,赋值对应的UI对象,
在这里插入图片描述
封装一个显示界面的接口,

public static void Show()
{
    var uiObj = UIMgr.instance.ShowUi("UIPrefabs/MainPanel");
    var panel = uiObj.GetComponent<MainPanel>();
    panel.OnShow();
}

OnShow中写UI的交互逻辑,请求题目的所有类别:

questionText.text = "正则请求服务器,请稍等...";
questionTypeDropdown.ClearOptions();
HttpHelper.instance.StartGetAllQuestionTypes((result) =>
{
    Debug.Log(result);
    var jd = JsonMapper.ToObject(result);
    List<string> options = new List<string>();
    // C#基础排最前面
    options.Add("C#基础");
    foreach (var item in jd)
    {
        var option = item.ToString();
        if ("C#基础" == option)
            continue;
        options.Add(option);
    }
    
    questionTypeDropdown.AddOptions(options);

    // ...
});

下一题按钮,

// 下一题按钮
nextBtn.onClick.AddListener(() =>
{
    ReqOneQuestion();
});

// ...


/// <summary>
/// 请求下一题
/// </summary>
private void ReqOneQuestion()
{
    var questionType = questionTypeDropdown.options[questionTypeDropdown.value].text;
    HttpHelper.instance.StartGetOneQuestion(questionType, (result) =>
    {
        var jd = JsonMapper.ToObject(result);
        var errorCode = int.Parse(jd["error_code"].ToString());
        if (0 == errorCode)
        {
            m_questionData = jd["data"];
            UpdateQuestionText(false);
        }
        else
        {
            questionText.text = "";
        }
    });
}

/// <summary>
/// 更新题目文本(可含答案)
/// </summary>
/// <param name="withAnswer">是否含答案</param>
private void UpdateQuestionText(bool withAnswer)
{
    if(withAnswer)
    {
        questionText.text = string.Format("题目:\n{0}\n{1}\n\n解答:\n{2}", m_questionData["question"].ToString(),
                m_questionData["code"].ToString(), m_questionData["answer"].ToString());
    }
    else
    {
        questionText.text = string.Format("题目:\n{0}", m_questionData["question"].ToString());
    }
}

看答案按钮,

// 看答案
answerBtn.onClick.AddListener(() =>
{
    UpdateQuestionText(true);
});

问题类别切换时,自动请求一道题,

// 问题类别切换
questionTypeDropdown.onValueChanged.AddListener((v) => 
{
    ReqOneQuestion();
});

题目录入按钮,

// 录入新题
inputQuestionBtn.onClick.AddListener(() =>
{
    QuestionInputPanel.Show(questionTypeDropdown.options, questionTypeDropdown.value);
});

这里要显示题目录入界面,所有们接下来写QuestionInputPanel脚本。

8、题目录入界面:QuestionInputPanel.cs

创建QuestionInputPanel.cs脚本,定义一些UI对象成员,

// 题目类别下拉框
public Dropdown questionTypeDropdown;
// 题目输入框
public InputField questionInput;
// 代码输入框
public InputField codeInput;
// 答案输入框
public InputField answerInput;
// 提交按钮
public Button okBtn;
// 返回按钮
public Button quitBtn;

QuestionInputPanel.cs挂到QuestionInputPanel.prefab预设上,赋值对应的UI对象,
在这里插入图片描述
封装一个显示界面的接口,

public static void Show(List<Dropdown.OptionData> options, int initQuestionType)
{
    var uiObj = UIMgr.instance.ShowUi("UIPrefabs/QuestionInputPanel");
    var panel = uiObj.GetComponent<QuestionInputPanel>();
    panel.OnShow(options, initQuestionType);
}

OnShow中实现界面交互逻辑,代码比较交单,我这里就不赘述了,

void OnShow(List<Dropdown.OptionData> options, int initQuestionType)
{
    questionTypeDropdown.ClearOptions();
    questionTypeDropdown.AddOptions(options);
    questionTypeDropdown.value = initQuestionType;

    // 提交题目到服务器
    okBtn.onClick.AddListener(() =>
    {
        var questionType = questionTypeDropdown.options[questionTypeDropdown.value].text;
        var question = questionInput.text;
        var code = codeInput.text;
        var answer = answerInput.text;
        if(string.IsNullOrEmpty(question))
        {
            FlyTips.Show("请输入题目");
            return;
        }
        if(string.IsNullOrEmpty(answer))
        {
            FlyTips.Show("请输入答案");
            return;
        }
        HttpHelper.instance.StartPostAddQuestion(questionType, question,
            code, answer, (result) =>
            {
                var jd = JsonMapper.ToObject(result);
                var errorCode = int.Parse(jd["error_code"].ToString());
                if(0 == errorCode)
                {
                    FlyTips.Show("试题录入成功");
                    questionInput.text = "";
                    codeInput.text = "";
                    answerInput.text = "";
                }
                else
                {
                    FlyTips.Show("服务器出错,录入失败");
                }
            });
    });

    // 返回按钮
    quitBtn.onClick.AddListener(() => 
    {
        Destroy(gameObject);
    });
}

上面我用到了一个Json库:LitJson,可以在GitHub中下载,地址:https://hub.fastgit.org/LitJSON/litjson

我们下载下来后,把src目录中的LitJson文件夹整个拷贝到我们Unity工程中,如下

使用时记得引入命名空间:

using LitJson;
9、冒提示语:FlyTips.cs

创建FlyTips.cs脚本,实现冒提示语的功能,代码如下,

using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 冒提示语
/// </summary>
public class FlyTips : MonoBehaviour
{
    public aniTrigger;
    public Text tipsText;

    public static void Show(string txt)
    {
        var uiObj = UIMgr.instance.ShowUi("UIPrefabs/FlyTips");
        var tips = uiObj.GetComponent<FlyTips>();
        tips.OnShow(txt);
    }

    private void OnShow(string txt)
    {
        tipsText.text = txt;
        aniTrigger.aniEvent = (msg) => 
        {
            if("finish" == msg)
            {
                Object.Destroy(gameObject);
            }
        };
    }
}

这里我封装了一个动画事件触发器:AnimationTrigger

using UnityEngine;
using System;

/// <summary>
/// 动画事件触发器
/// </summary>
public class AnimationTrigger : MonoBehaviour
{
    public Action<string> aniEvent;

    public void TriggerEvent(string msg)
    {
        aniEvent?.Invoke(msg);
    }
}

因为提示语冒完后要销毁(你也可以使用对象池回收起来,下次复用),所以我们需要监听动画的结束,我们先给动画添加帧事件,
在这里插入图片描述
帧事件的响应函数选择TriggerEvent,字符串参数填finish
在这里插入图片描述
这样帧事件就会触发AnimationTriggerTriggerEvent函数调用,进而调用委托Action<string> aniEvent,我们只需要在FlyTips中设置这个委托即可实现动画帧事件触发逻辑了。

以上,基本就是客户端部分的内容了,接下来讲下服务端部分。

六、Web服务端部分

服务端部分是一个Web服务器,我用python来写,pythonWeb框架有webpytornado等等,我推荐使用tornado,我之前写过一篇关于tornado搭建Web服务器的文章:《使用Python Tornado搭建web服务器》,感兴趣的同学可以看看。
tornado官网:https://www.tornadoweb.org/en/stable/index.html#
tornado安装:https://pypi.org/project/tornado/

1、服务器思维图

在这里插入图片描述
我们创建一个web_server.py脚本,创建一个question_bank目录用于存放题库,如下:
在这里插入图片描述
question_bank目录文件如下:
在这里插入图片描述

2、启动一个Web服务器

使用tonado启动一个Web服务器非常的简单,下面就是一个最简单的Hello World例子,

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

我们通过http://localhost:8888即可访问Web服务器了。

3、跨域访问问题的解决

如果你的Web服务器搭建与你的客户端不同的局域网中,则访问时会出现跨域反问问题,解决办法是定义BaseHandler,设置header,然后所有的Handler类都继承BaseHandler,例:

import tornado.ioloop
import tornado.web
from tornado.web import RequestHandler

class BaseHandler(tornado.web.RequestHandler):
    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")   # 这个地方可以写域名
        self.set_header("Access-Control-Allow-Headers", "x-requested-with")
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')

    def post(self):
        self.write('some post')

    def get(self):
        self.write('some get')

    def options(self):
        # no body
        self.set_status(204)
        self.finish()

class MyHandler(BaseHandler):
    def get(self):
        self.write('hello world')
    def post(self):
        self.write('hello world')
4、读取所有题目

服务器启动时,先读取所有的题目到内存中(因为题目量不大,所以就全部一次性读进来,如果题目海量,则不建议这么做),

all_questions = {}

# 读取所有的题目
def read_all_questions():
    global all_questions
    for root, dir, fs in os.walk('question_bank'):
        for f in fs:
            if f.endswith('.json'):
                fpath = os.path.join(root, f)
                fr = open(fpath, 'r', encoding='utf-8')
                txt = fr.read()
                if '' == txt:
                    txt = '[]'
                fr.close()
                f = f.replace('.json', '')
                all_questions[f] = json.loads(txt)
5、请求所有的题目类型

请求所有题目的类别,需要注意的就是json.dumpsensure_ascii参数要设置为False,否则传回客户端时中文的编码会有问题,

# 请求所有的题目类型
class get_question_types(BaseHandler):
    def get(self):
        question_types = []
        for k in all_questions.keys():
            question_types.append(k)
        self.write(json.dumps(question_types, ensure_ascii=False))
6、随机获取一道题目

这里主要是通过self.get_argument获取客户端Get请求的参数,根据题目类别从题库容器中随机抽取一道题,最后以json的格式返回给客户端,

# 随机获取一道题目
class get_one_question(BaseHandler):
    def get(self):
        question_type = self.get_argument('question_type', default = 'C#基础')
        if not question_type in all_questions.keys():
            self.write('{ "error_code" : 1 }')
            return
        questions = all_questions[question_type]
        if 0 == len(questions):
            self.write('{ "error_code" : 1 }')
            return
        index = random.randint(0, len(questions) - 1)
        question = questions[index]
        question_txt = json.dumps(question, ensure_ascii=False)
        self.write('{ "error_code" : 0, "data" : %s }'%question_txt)
7、录入一道题目

这里主要是通过self.get_argument获取客户端Post请求的参数,然后更新内存题库数据并回写到本地json文件中,

# 录入一道题目
class add_one_question(BaseHandler):
    def post(self):
        global all_questions
        question_info = {}
        question_type = unquote(self.get_argument('question_type'))
        question_info['question'] = unquote(self.get_argument('question'))
        question_info['code'] = unquote(self.get_argument('code'))
        question_info['answer'] = unquote(self.get_argument('answer'))
        all_questions[question_type].append(question_info)
        f = open('./question_bank/' + question_type + '.json', 'w', encoding='utf-8')
        f.write(json.dumps(all_questions[question_type], ensure_ascii=False, sort_keys=False, indent=2))
        f.close()
        self.write('{ "error_code" : 0 }')
8、web_server.py完整代码

最终服务器完整代码如下:

import os
import json
import random
from urllib.parse import unquote
import tornado.ioloop
import tornado.web
from tornado.web import RequestHandler

all_questions = {}

class BaseHandler(tornado.web.RequestHandler):
    # 解决跨域访问问题
    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")   # 这个地方可以写域名
        self.set_header("Access-Control-Allow-Headers", "x-requested-with")
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')

    def post(self):
        self.write('some post')

    def get(self):
        self.write('some get')

    def options(self):
        # no body
        self.set_status(204)
        self.finish()

# 请求所有的题目类型
class get_question_types(BaseHandler):
    def get(self):
        question_types = []
        for k in all_questions.keys():
            question_types.append(k)
        self.write(json.dumps(question_types, ensure_ascii=False))

# 随机获取一道题目
class get_one_question(BaseHandler):
    def get(self):
        question_type = self.get_argument('question_type', default = 'C#基础')
        if not question_type in all_questions.keys():
            self.write('{ "error_code" : 1 }')
            return
        questions = all_questions[question_type]
        if 0 == len(questions):
            self.write('{ "error_code" : 1 }')
            return
        index = random.randint(0, len(questions) - 1)
        question = questions[index]
        question_txt = json.dumps(question, ensure_ascii=False)
        self.write('{ "error_code" : 0, "data" : %s }'%question_txt)

# 录入一道题目
class add_one_question(BaseHandler):
    def post(self):
        global all_questions
        question_info = {}
        question_type = unquote(self.get_argument('question_type'))
        question_info['question'] = unquote(self.get_argument('question'))
        question_info['code'] = unquote(self.get_argument('code'))
        question_info['answer'] = unquote(self.get_argument('answer'))
        all_questions[question_type].append(question_info)
        f = open('./question_bank/' + question_type + '.json', 'w', encoding='utf-8')
        f.write(json.dumps(all_questions[question_type], ensure_ascii=False, sort_keys=False, indent=2))
        f.close()
        self.write('{ "error_code" : 0 }')

# 创建tornado web服务器对象
def make_app():
    return tornado.web.Application([
        (r"/get_question_types", get_question_types),
        (r"/get_one_question", get_one_question),
        (r"/add_one_question", add_one_question),
    ])

# 读取所有的题目
def read_all_questions():
    global all_questions
    for root, dir, fs in os.walk('question_bank'):
        for f in fs:
            if f.endswith('.json'):
                fpath = os.path.join(root, f)
                fr = open(fpath, 'r', encoding='utf-8')
                txt = fr.read()
                if '' == txt:
                    txt = '[]'
                fr.close()
                f = f.replace('.json', '')
                all_questions[f] = json.loads(txt)
    
    print('read_all_questions ok')

if __name__ == "__main__":
    read_all_questions()
    app = make_app()
    app.listen(7891)
    print('localhost:7891')
    tornado.ioloop.IOLoop.current().start()

七、运行测试

1、启动服务器

使用python执行web_server.py,如下:
请添加图片描述

2、客户端刷题

运行Unity客户端,测试刷题,效果如下:
请添加图片描述

3、录入题目

测试录入题目,
请添加图片描述
接着我们就可以在题库中刷到新题啦,
在这里插入图片描述
录入的题库会存在json文件中,如下
在这里插入图片描述

八、工程源码

本工程源码我已上传到CODE CHINA,地址:https://codechina.csdn.net/linxinfa/UnityQuestionRank,感兴趣的同学可自行下载学习。
注,我使用的Unity版本是Unity 2021.1.9f1c1 (64-bit)

在这里插入图片描述

九、完毕

好了,就写到这里吧,目前题库题量还比较少,有空了我再进行完善,如果有热心的朋友帮我录入题目那就感激不尽啦~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,拜拜~

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐