游戏开发题库使用Unity制作Unity题库,支持题目录入和刷题(面试 | 笔试 | 自制题库 | 从基础到高级)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发题库使用Unity制作Unity题库,支持题目录入和刷题(面试 | 笔试 | 自制题库 | 从基础到高级)相关的知识,希望对你有一定的参考价值。
本文最终效果如下:
文章目录
一、前言
嗨,大家好,我是新发。
我最近在找一些Unity
面试题,然后我看到,有一些网站和小程序的答题需要钱,我比较穷,于是我决定自己做一个题库录入和刷题的程序,自给自足,方便自己整理题目,也顺便教一下大家,看看我是如何使用Unity
制作Unity
题库的。
二、方案设计
我想做的功能很简单,就是客户端录入题目,按题目分类存到服务端,客户端可以选择不同类别的题目进行随机刷题。
画个图:
客户端部分我使用Unity
来做,服务端我准备使用python
来写,使用tornado
的Web
框架,题库数据库就使用简单的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 - Camera
,Render Camera
设置为主摄像机,
Canvas Scaler
组件的UI Scale Mode
设置为Scale With Screen Size
,Reference Resolution
设置为1280 * 720
,如下:
3、制作界面预设
根据界面设计,制作界面预设。
3.1、刷题界面:MainPanel.prefab
层级结构如下:
3.2、题目录入界面:QuestionInputPanel.prefab
层级结构如下:
3.3、提示语:FlyTips.prefab
再做一个提示语的预设,
层级结构如下,一个黑色背景图为父节点,文字为子节点,
提示语的背景需要根据文字自适应,
要实现上面的自适应新效果,只需在背景图挂Content Size Fitter
和Horizontal Layout Group
组件,
其中Content Size Fitter
的Horizontal Fit
设置为Preferred Size
,因为我们只需要做横向自适应,
Horizontal Layout Group
组件的Control Child Size
勾选Width
,这样文字子物体的宽度就可以控制背景图的宽度了,把Child Alignment
设置为Middle Center
,这样文字就居中对齐了,再设置一下Padding
的Left
和Right
,让背景图的左右两侧留一些空白,
顺手给提示语预设做个动画,
动画文件记得把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接口
}
}
接下来我们封装一下Http
的Get
接口和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
编码,Unity
在UnityWebRequest
类中提供了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 一个拼图demo(七巧板)和一个切割demo—2