先搭建好ui,需要的内容大概是一个横版,然后创建一个scrow view,使用其中的vertical,创建一个任务物体(里面存放单个任务的ui,如框,文字等),保存为一个预制体放在resources文件里方便后期通过代码自动生成,从而做出简单的任务列表
需要在数据库中添加一个用户表和一个任务表,两者用一个id进行绑定,我这里是用的missionId进行多表查询。这里可以看表设计相关的知识,篇幅和时间有限就不多讲了,任务表里需要添加你需要的字段,如任务名称,任务状态(这里我只需要未完成和完成,所以用0和1代替即可),以及任务描述等信息
先创建任务类,这个类不需要继承mono,我们只放构造函数在里面,添加需要的字段,这里可以和数据库里的相匹配,名称,状态啥的
public class Quest
{
public string questName;
public Quest(string _questInfo, int _aCoins,int _questStatus)
{
this.questInfo = _questInfo;
this.aCoins = _aCoins;
this.questStatus = _questStatus;
}
public int aCoins {
get; set; }
public int questStatus {
get; set; }
public string questInfo {
get; set; }
}
现在本地查询,如果本地没有的话,则在程序启动的时候,需要根据用户查询用户的任务表信息,并写在本地(减少访问数据库的次数,提高程序流畅感),根据用户查询用户的方法由于需要和as的开发大佬沟通,这里现在只是做demo,所以用户先在查询语句中定死一个即可
如果本地已经有任务数据的话,则直接访问本地的数据即可
将查询回来的数据进行分析处理,获得我们需要的数据,将数据保存到任务集合中,查询任务信息的时候就访问这个集合即可
将任务信息的集合里的数据同步在我们前面创建好的预制体里的组件即可
然后instantiate创建数据库中已有的任务列表即可
由于任务信息是和人物相关的,所以任务的信息列表我是放在了人物的脚本中的
先在awake方法中调用查询文件的方法,我是创建了一个专门读写文件的类,单例之后去调用单例模式下的文件读写的读方法
如果本地有任务信息的话,则将本地的任务信息更新给人物里存放任务信息的字典中(集合也可以,不过字典可以直接通过key替换values 的值,list要换,可能是我不熟悉,所以个人还是用的字典)
这里在拿到本地的数据后我对数据做了一些判断,因为在数据库中任务类型我用的是int整型,不过字典的key需要英文方便代码查看,所以我做了一个对数据的判断,不论是本地读取还是云读取,都是需要判断的,后面可以整合到一个方法里进行代码优化
public class PlayerInfo : MonoBehaviour
{
public static PlayerInfo _instance;
public int aCoins;
public Dictionary<string,Quest> questList = new Dictionary<string,Quest>();//任务状态目前计划从数据库中获取完成情况
public static PlayerInfo Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType(typeof(PlayerInfo)) as PlayerInfo;
}
return _instance;
}
}
/*public void GetMissionStatus()
{
//从服务器获取任务完成情况
//questList.Add("ListenStory",new Quest("ListenStory", Quest.QuestStatus.Accepted));
//调用consql脚本,通过多表查询获得任务的最新情况并赋值给questlists
}*/
private void Awake()
{
FileManager.Instance.ReadFile("MissionInfo.txt");
}
///
/// 初始化任务列表
///
public void InstantiateMissionData()
{
Debug.Log(FileManager.Instance.readTempData);
if (!FileManager.Instance.readTempData.Contains("questInfo"))//如果本地没有任务列表的数据,默认是第一次进入,应该从云端拉取数据
{
string data = ConSql.Instance.SelectData("*", "missionInfo", "missionId", "1");//获得服务器上任务数据,进行json格式的解析
MissionInfoInstance.Root mis = JsonMapper.ToObject<MissionInfoInstance.Root>(data);
string missionName = "";
for (int i = 0;i<mis.rows.Count; i++)
{
switch (i)
{
case 0:
missionName = "StoryCenter";
break;
case 1:
missionName = "LessonCenter";
break;
case 2:
missionName = "PKCenter";
break;
case 3:
missionName = "ChallengeCenter";
break;
case 4:
missionName = "ExpCenter";
break;
case 5:
missionName = "EvaCenter";
break;
}
questList.Add(missionName, new Quest(mis.rows[i].missionDes, int.Parse(mis.rows[i].missionCoin), int.Parse(mis.rows[i].missionStatus)));
}
//string questInfo = "听取一则故事";
//questList.Add("StoryCenter", new Quest(mis.rows., 1,0));
//questInfo = "观看一个课程";
//questList.Add("LessonCenter", new Quest(questInfo, 2,0));
//questInfo = "参与大擂台";
//questList.Add("PKCenter", new Quest(questInfo, 3,0));
//questInfo = "完成一次挑战打卡";
//questList.Add("ChallengeCenter", new Quest(questInfo, 4,0));
//questInfo = "完成一次小游戏";
//questList.Add("ExpCenter", new Quest(questInfo, 5,0));
//questInfo = "进入测评中心";
//questList.Add("EvaCenter", new Quest(questInfo, 6,0));
FileManager.Instance.WriteFile(data, "MissionInfo.txt");
}
else//有本地数据读取本地任务列表
{
MissionInfoInstance.Root mis = JsonMapper.ToObject<MissionInfoInstance.Root>(FileManager.Instance.readTempData);
//questList.Add("StoryCenter", new Quest(mis.StoryCenter.questInfo, mis.StoryCenter.aCoins,mis.StoryCenter.questStatus));
string missionName = "";
for (int i = 0; i < mis.rows.Count; i++)
{
switch (i)
{
case 0:
missionName = "StoryCenter";
break;
case 1:
missionName = "LessonCenter";
break;
case 2:
missionName = "PKCenter";
break;
case 3:
missionName = "ChallengeCenter";
break;
case 4:
missionName = "ExpCenter";
break;
case 5:
missionName = "EvaCenter";
break;
}
questList.Add(missionName, new Quest(mis.rows[i].missionDes, int.Parse(mis.rows[i].missionCoin), int.Parse(mis.rows[i].missionStatus)));
}
}
}
}
文件读写类
有关文件读写的问题
真机里面调用读写的话,最好是用application.persistendatapath这个路径
并且读的时候用这个
写的时候需要在这个路径前加上“file://”,否则是写不进去的~
还有一个需要注意的
由于读取数据的时候是需要时间的, 但是人物类里对数据进行判断的时候是在awake里进行的话,会出现数据还没读取完人物类查询数据后的方法就在执行了,就会出现一直查不到数据,就会一直从服务器读取数据,其实是没必要的,所以这里我把人物类查询数据的方法放在了读取数据的协程里面,这样就能保证读取完之后才执行对数据进行判断处理的方法
这里和协程的进行有关系,简单的说就是,协程是多线程操作,当你用这个方法的时候相当于开多了一条线程,但是主线程还在运行,所以主线程的工作不会等到你协程的工作做完才进行,就会出现上面的这个问题。所以如果有一定要在什么方法后面执行的方法,并且是有协程的,一定要把方法写在协程里面才行
using System.Collections;
using System.Collections.Generic;
using System.IO;
using LitJson;
using UnityEngine;
///
/// 用来存放读写文件的方法的类
///
public class FileManager : MonoBehaviour
{
public string readTempData = "";//暂时存放读取数据的声明
public static FileManager _instance;//单例模式
public static FileManager Instance
{
get{
if (_instance == null)
{
_instance = FindObjectOfType(typeof(FileManager)) as FileManager;
}
return _instance;
}
}
///
/// 写入文件的方法
///
///
///
public void WriteFile(string file,string fileName)
{
string path = Application.persistentDataPath + "/" + fileName;
FileStream sf = new FileStream(path,FileMode.Create,FileAccess.Write);//打开文件流
//string data = JsonMapper.ToJson(file);//文件转成json
byte[] dataStream = System.Text.Encoding.ASCII.GetBytes(file);//json转byte字节流
sf.Write(dataStream,0,dataStream.Length);//写入文件
//关闭文件流
sf.Close();
sf.Dispose();
}
///
/// 用来读取文件的方法
///
///
public void ReadFile(string fileName)
{
if (Application.platform == RuntimePlatform.Android)
{
StartCoroutine(ReadByWWW("file://" + Application.persistentDataPath + "/" + fileName));
}else if (Application.platform == RuntimePlatform.WindowsEditor)
{
StartCoroutine(ReadByWWW(Application.persistentDataPath + "/" + fileName));
}
}
IEnumerator ReadByWWW(string path)
{
WWW www = new WWW(path);//直接用www进行访问
yield return www;
readTempData = www.text;
PlayerInfo.Instance.InstantiateMissionData();
}
}
连接数据库查询的类
这个类还是之前写过的链接数据库帖子里的,不过做了点修改,那个帖子好像涉及了版权问题,不给我通过,所以就设为私密啦~
之前只是简单的查询数据,但是现在需要查询并返回多个结果,用之前的dataset里的tables[0].rows[0][0]这样方法太不科学了,所以用了网上一个大佬把数据转换成json格式的方法,然后通过给这个json格式创建一个类去封装他,从而更方便的调用里面的数据来实现获得多个结果的功能
方法就在下面的datatojson
https://www.cnblogs.com/beast-king/p/4351535.html
这个大佬还写了其他的转换的方法,时间关系我只借鉴了第一个,在这里表示感谢~如果有版权问题请通知小弟,看到消息我会及时删除的
public class ConSql : MonoBehaviour
{
// Start is called before the first frame update
string constr = "Database = 数据库名称; Data Source = 数据库ip; User Id = root; Password = ; port = 3306";
void Start()
{
}
public static ConSql _instance;
public static ConSql Instance
{
get
{
if (_instance==null)
{
_instance = FindObjectOfType(typeof(ConSql)) as ConSql;
}
return _instance;
}
}///
/// 查询的方法
///
///
///
///
///
///
public string SelectData(string sheet,string db,string field,string key)
{
//建立连接
MySqlConnection cmd = new MySqlConnection(constr);
//打开连接
cmd.Open();
//查询数据
string selstr = string.Format("select {0} from {1} where {2} = '{3}';", sheet, db,field, key);
MySqlCommand myselect = new MySqlCommand(selstr, cmd);
DataSet ds = new DataSet();
DataTable dt = new DataTable();
try
{
MySqlDataAdapter da = new MySqlDataAdapter(selstr, cmd);
da.Fill(ds);
da.Fill(dt);
//print("Query success!");
//print(ds.Tables[0].Rows[0][0]);
return DataToJson(dt,dt.Rows.Count);
}
catch (Exception e)
{
throw new Exception("SQL:" + selstr + "\n" + e.Message.ToString());
}
//关闭连接
cmd.Close();
}
public static string DataToJson(DataTable dt, int count)
{
StringBuilder sbjson = new StringBuilder();
sbjson.Append("{");
sbjson.Append("\"total\":" + count + ",\"rows\":[");
if (dt != null)
{
for (int i = 0; i < dt.Rows.Count; i++)
{
if (i > 0)
{
sbjson.Append(",");
sbjson.Append("{");
foreach (DataColumn dc in dt.Columns)
{
if (dt.Columns.IndexOf(dc) > 0)
{
sbjson.Append(",");
sbjson.Append("\"" + dc.ColumnName + "\":\"" + dt.Rows[i][dc.ColumnName].ToString().Trim() + "\"");
}
else
{
sbjson.Append("\"" + dc.ColumnName + "\":\"" + dt.Rows[i][dc.ColumnName].ToString().Trim() + "\"");
}
}
sbjson.Append("}");
}
else
{
sbjson.Append("{");
foreach (DataColumn dc in dt.Columns)
{
if (dt.Columns.IndexOf(dc) > 0)
{
sbjson.Append(",");
sbjson.Append("\"" + dc.ColumnName + "\":\"" + dt.Rows[i][dc.ColumnName].ToString().Trim() + "\"");
}
else
{
sbjson.Append("\"" + dc.ColumnName + "\":\"" + dt.Rows[i][dc.ColumnName].ToString().Trim() + "\"");
}
}
sbjson.Append("}");
}
}
}
sbjson.Append("]}");
return sbjson.ToString();
}
}
再进行说明的话流程就是
人物类启动文件读写类里读取本地任务信息列表的方法
读取信息列表的方法读取数据,然后将数据储存到这个类里的data字段,然后再在这个协程里调用人物类里的任务信息初始化的方法
通过这个流程能确保任务数据是在本地查询过的
然后在人物初始化任务信息的方法里进行判断,如果本地有数据,则将本地的数据进行json.toobject的方法,将数据封装到实体类里,然后循环遍历将数据写入人物类里负责存放任务列表的字典里
如果本地没有数据的话,则需要调用链接数据库的类进行查询,将返回的数据转换成json格式,一样封装到实体类里,一样循环遍历数据到任务列表的字典里
然后当点击任务这个点击事件发生的时候,missionmanager这个类就发送消息给uimanager类,让他将最新的任务列表的信息显示在ui中
uimanager类里进行任务列表显示的方法如下,我还尝试了给不同的任务状态添加不同的高亮效果,是在之前b站大神joe那里学的(这个有点类似前端)
public void ShowMissionDetails()
{
Dictionary<string, Quest> questLists = PlayerInfo.Instance.questList;
GameObject content = GameObject.Find("MissionSystem").transform.GetChild(1).GetChild(0).GetChild(0).gameObject;
if (content.transform.childCount==0)
{
GameObject questGO = new GameObject();
foreach (var key in questLists.Keys)
{
questGO = GameObject.Instantiate(Resources.Load("Quest", typeof(GameObject)) as GameObject);
questGO.transform.GetChild(0).GetChild(0).GetComponent<Text>().text = key;
questGO.transform.GetChild(0).GetChild(1).GetComponent<Text>().text = questLists[key].questInfo + " +" + questLists[key].aCoins + "A币";
if (questLists[key].questStatus == 0)
{
questGO.transform.GetChild(1).GetChild(0).GetComponent<Text>().text = "否 ";
}
else
{
questGO.transform.GetChild(1).GetChild(0).GetComponent<Text>().text = "是 ";
}
questGO.transform.SetParent(GameObject.Find("MissionSystem").transform.GetChild(1).GetChild(0).GetChild(0), false);
}
}
else
{
int count = 0;
foreach (var key in questLists.Keys)
{
if (questLists[key].questStatus == 0)
{
content.transform.GetChild(count).GetChild(1).GetChild(0).GetComponent<Text>().text = "否 ";
}
else
{
content.transform.GetChild(count).GetChild(1).GetChild(0).GetComponent<Text>().text = "是 ";
}
count++;
}
}
}
missionmanager类
这个类就只是负责发送消息的,目前只发送给uimanager,后面还需要发送给音效管理和动画特效管理等
//负责发送任务事件
public delegate void MissionUIEventHandler();//声明一个任务与ui的委托
public class MissionManager : MonoBehaviour
{
public static MissionManager instance;
public static MissionManager Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType(typeof(MissionManager)) as MissionManager;
Debug.Log("mmisOn");
}
return instance;
}
}
//创建任务与ui的事件
public event MissionUIEventHandler MTU;
public void SentMessageToOther()
{
//this.MTU += new MissionUIEventHandler(PlayerInfo.instance.GetMissionStatus);//从服务器拉取任务完成情况
//发送消息给任务监听者,检查是否有任务状态更新
//发送消息给ui管理,进行任务界面的显示
this.MTU += new MissionUIEventHandler(UIManager.Instance.MS);
MTU();
}
}
在我的项目里我还添加模拟了点击按钮修改任务状态,然后能够实时更新到任务列表里的功能,仅供测试,因为是需要和as那边的大佬协商的,这里只做测试,就不展示了,流程其实差不多,既然ui都是从人物类的任务列表里进行数据查询和显示的,只需要在点击事件的时候获得人物类里的任务列表字典就可以进行修改了~
以上是我结合最近看的委托、事件,数据库查询,脚本生命周期,主协程等知识做的一个比较综合的demo,其实还有很多不够完善,比如如果是新用户的话在数据库里是没有相应的任务列表信息的,还需要添加插入的sql语句给数据库里的用户和任务表进行关联;再比如查询数据库的类里目前只能对任务数据进行查询,做不到泛华,也即是查询的数据一旦更改,就会直接在封装给实体类的时候报错了;还有的就是一些ui的优化了~
路漫漫其修远兮,加油~