终于写到了最后一篇。对应的前后端源码和可执行程序,unitypackage已放在github上,欢迎大
家参考。最后一篇是关于Unity中的程序设计思路。虽然在这个课设任务中前端并不是最重要的考查目标,但其中用到了一些很好的想法也在这里分享下。( 要完结了,撒花!)数据结构大作业-DBLP科学文献管理系统-概述 (C++/C#/Unity)_Sugarzo_mei的博客-优快云博客
C#中的线程、委托应用
由于前端中是调用后端dll库,而有些函数运行起来需要很长时间,比如建库函数3G的数据,就要执行半个小时。如果只是将调用简单的写在unity主线程中,那unity就会直接无响应,这时候就需要引入线程思想。将后端dll的函数放在线程中调用,就不会影响到主线程帧率的刷新了,也支持在程序中显示进度。
这里提醒一句,由于C#是线程不安全性语言,在使用线程时需要考虑资源互斥,死锁等现象。如果涉及到多线程同时工作,必要时需要引入lock等关键字对线程资源进行控制(不过在这里只需要简单使用,就不需要考虑这么多了)
使用线程,我们需要先引入命名空间:
using System.Threading;
然后新建线程的引用变量,这里我设置了一个线程的静态管理类方便调用,并设置了对应的委托和回调事件:
public static class ThreadManager
{
private static Thread thread = null;
/// <summary>
/// 开始一个线程
/// </summary>
/// <param name="runlogic">线程运行逻辑</param>
/// <param name="callback">线程执行完成后,回调函数主线程逻辑</param>
public static void StartThread(Action runlogic,Action callback)
{
if(thread != null)
{
if (thread.IsAlive)
thread.Abort();
}
Debug.Log("新建进程");
thread = new Thread(delegate() { ThreadRun(runlogic, callback); });
thread.Start();
}
private static void ThreadRun(Action runlogic, Action callback)
{
Debug.Log("进入进程");
runlogic?.Invoke();
Debug.Log("进程执行完成,进入回调函数");
MainObject.Instance.ThreadAction += delegate () //将回调事件委托,写入主线程
{
callback?.Invoke();
Debug.Log("回调函数完成,结束进程");
};
thread.Abort(); //终止线程
}
}
值得注意的时,在unity编辑模式下,即使游戏状态不在运行,脚本中的线程也可能在执行,因此使用完时留下abort()的好习惯。
受到Unity引擎的限制,Unity自带的API只能在主线程中使用。比如当你的线程计算完成时,如果需要刷新对应的UI或者修改游戏组件,需要将事件通知到主线程中。 在上文中还涉及到一个mainObject的类,它的结构是这样的
public class MainObject : SingletonMono<MainObject>
{
public Action ThreadAction = null;
private void Update()
{
if(ThreadAction != null)
{
ThreadAction.Invoke();
ThreadAction = null;
}
}
}
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
public static T Instance;
protected virtual void Awake()
{
if (Instance == null)
{
Instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
if (Instance != this)
{
Destroy(gameObject);
}
}
}
}
可以看到这里实现了一个单例模式,并通过一个Action委托在update中持续检测。当线程计算完成后,将事件在action中注册,就可以通过主线程修改游戏组件的数据/刷新对应的UI啦!
状态机设计
状态机是指程序在有限个状态种互相切换的模型,是程序设计中非常常见的设计模式!养成良好的编码习惯是程序员的必备需求。状态机有着容易理解,维护方便的优点。实际使用的状态机根据使用需求可以设计出很多种类,这里因为只是一个小程序,讲解一个最简单的状态机模型。
我们拿这个建库界面的需求作为分析,实际上他根据不同输入条件,有很多种不同的状态,我们使用一个枚举将它列举出来:
public enum State
{
Null, //未设置的初始值
Idle, //默认
Checking, //检查路径中
Building, //建库中
Finish, //数据库正确,准备进入程序
Error, //路径错误,需要重新配置路径
Ready, //数据库未建立,但是有dblp,进入准备建库状态
}
然后我们实现一个简单的状态表示(这里我使用了抽象类,也可以用Interface接口进行实现),一个状态最简单事件应该有:进入时/退出时/执行时(这里只需要控制对应的UI组件,事件是瞬发的,因此执行时也省去了)
public abstract class IState
{
public abstract void OnEnter(); //状态进入时
public abstract void OnExit(); //状态退出时
}
//这里拿finish状态作为例子,继承基类,可以在状态事件中设置对应的UI控制
private class State_Finish : IState
{
public override void OnEnter()
{
Instance.State_dblp.SetState(StateIcon.State.Finish);
Instance.State_database.SetState(StateIcon.State.Finish);
GlobalManager.filePath = Instance.path;
Instance.Enter.interactable = true;
Instance.Loading.gameObject.SetActive(false);
}
public override void OnExit()
{
Instance.Enter.interactable = false;
}
}
最后我们用字典存储状态,再设置好对应的状态转移函数(执行前一个状态的退出事件再执行当前状态的进入事件),在start或者enable中注册事件,一个简单的状态机模型就设置好了。
public State programState = State.Null; //程序当前状态
private Dictionary<State, IState> StateMachine;
public void FSM(State state) //切换状态函数
{
if (state == programState) //如果状态没有变化
return;
StateMachine[programState].OnExit(); //执行前一个状态的退出事件
programState = state; //更换状态
StateMachine[programState].OnEnter();//执行下一个状态的进入事件
}
private void OnEnable()
{
if(StateMachine == null)
{
StateMachine = new Dictionary<State, IState>();
//注册状态机
StateMachine.Add(State.Null, new State_Empty());
StateMachine.Add(State.Idle, new State_Idle());
StateMachine.Add(State.Building, new State_Building());
StateMachine.Add(State.Checking, new State_Checking());
StateMachine.Add(State.Error, new State_Error());
StateMachine.Add(State.Finish, new State_Finish());
StateMachine.Add(State.Ready, new State_Ready());
}
FSM(State.Idle);
}
搜索栏GUI
程序中实现的搜索栏主要功能为:下拉菜单、滚动区域、以及根据用户点击的内容可以调跳转到对应的搜索(作者名->作者搜索、标题名->文章搜索)
这里需要使用ScrollView,绑定好对应的scrollRect组件,保留垂直滑动功能。
需要保证这个高度是随着一个搜索模块的动态变化的,搜索出来的结果也需要垂直排列,所以Content要加上ContentSizeFitter组件,设置垂直适应为PreferredSize,以及配置VecticalLayoutGroup。
然后设置好对应的搜索框预制体组件,这里添加TmpText和button组件实现跳转
当搜索按钮被按下时,判断当前选择的搜索选项,调用线程执行对应的dll接口,得到结果后以content为父节点,实例化该预制体,设置对应的文件即可(如果可以也可以加入对象池进行优化)
public class SearchManager : SingletonMono<SearchManager>
{
/// <summary>
/// 当搜索按钮被按下时
/// </summary>
[Button]
private void OnSearchButtonClick()
{
if (string.IsNullOrEmpty(inputField.text))
return;
SetState(SearchState.Searching);
if(thread != null)
{
if(thread.IsAlive)
thread.Abort();
Debug.Log("更换线程");
}
if(dropdown.captionText.text == "文章搜索")
{
thread = new Thread(DLL_SearchTitle);
}
if (dropdown.captionText.text == "作者搜索")
{
thread = new Thread(DLL_SearchAuthor);
}
if (dropdown.captionText.text == "模糊搜索")
{
thread = new Thread(DLL_SearchFuzzy);
}
if (dropdown.captionText.text == "聚团编号搜索")
{
thread = new Thread(SearchAua);
}
thread.Start();
}
/// <summary>
/// 外部调用OnSearchButtonClick()接口
/// </summary>
public void Search(string input,int type)
{
inputField.text = input;
dropdown.value = type;
//dropdown.captionText.text = searchType;
OnSearchButtonClick();
}
}
这里的点击属性,调用对应的搜索跳转功能,使用了Lambda表达式,在对应的button.Onclick中添加搜索事件即可
if (buttonActionType == TitleColorSet.SearchActionType.Article)
{
obj.GetComponent<Button>().enabled = true;
obj.GetComponent<Button>().onClick.AddListener(delegate ()
{
SearchManager.Instance.Search(tex, 0);
});
}
if (buttonActionType == TitleColorSet.SearchActionType.Author)
{
obj.GetComponent<Button>().enabled = true;
obj.GetComponent<Button>().onClick.AddListener(delegate ()
{
SearchManager.Instance.Search(tex, 1);
});
}