游戏开发题库使用Unity制作Unity题库,支持题目录入和刷题(面试 | 笔试 | 自制题库 | 从基础到高级)

Posted 林新发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发题库使用Unity制作Unity题库,支持题目录入和刷题(面试 | 笔试 | 自制题库 | 从基础到高级)相关的知识,希望对你有一定的参考价值。

本文最终效果如下:


一、前言

嗨,大家好,我是新发。
我最近在找一些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(以上是关于游戏开发题库使用Unity制作Unity题库,支持题目录入和刷题(面试 | 笔试 | 自制题库 | 从基础到高级)的主要内容,如果未能解决你的问题,请参考以下文章

Unity中制作游戏的快照游戏支持玩家拍快照

用unity3d制作游戏的时候用得上Python技术吗?

什么是unity3d?

unity 一个拼图demo(七巧板)和一个切割demo—2

unity制作赛车游戏,车子加了刚体和碰撞体为啥车子死活不动?

Unity实战100例Unity万能答题系统之单选多选判断题全部通用