用Unity還原星露谷物語(yǔ)(2):種地篇(數(shù)據(jù)讀寫+TileMap)

本文作者 :對(duì)馬騎馬使用炎拳
大家好,我是想對(duì) 使用炎拳的炎拳,上一篇星露谷中我完成了人物的移動(dòng)并搭建了一個(gè)簡(jiǎn)單的界面,這篇就來瞅瞅星露谷的地是怎么種的。
首先看看游戲中的種地的過程:

大致上完成一次耕種,需要這幾個(gè)步驟:選中鋤頭—開墾一塊地—選擇種子種下—澆水;同時(shí)種子會(huì)隨時(shí)間的流逝成長(zhǎng),長(zhǎng)到有葉片后人物走過,會(huì)觸發(fā)一個(gè)莊稼動(dòng)一下的動(dòng)畫~
所以這里我要做的功能就清晰了,先整個(gè)莊稼的預(yù)制體,其中包含所需的圖片文字素材,然后點(diǎn)擊一下土地就在那位置生成一個(gè),大功告成啦!
才怪~

考慮到星露谷的物品繁多,如果用預(yù)制體來制作每個(gè)單獨(dú)的物品,就太笨拙了;同時(shí)每個(gè)物品有自己的描述和各種屬性,對(duì)應(yīng)的Sprites也可以通過其地址實(shí)時(shí)讀取,所以我希望能通過表格來填寫這些物品的固有屬性,這些數(shù)據(jù)屬于靜態(tài)數(shù)據(jù)。
同時(shí)種子,魚,這些物品使用時(shí)會(huì)有數(shù)量的增減,而斧頭,鋤頭這些道具則可以進(jìn)行升級(jí),數(shù)量,等級(jí)這些數(shù)據(jù)屬于動(dòng)態(tài)數(shù)據(jù),經(jīng)常改變,游戲結(jié)束也需要保存下來。
所以最后我的方案是:物品的名字,描述,Sprites地址等靜態(tài)數(shù)據(jù)通過表格獲得,表格的讀取使用EPPlus插件實(shí)現(xiàn);物品的數(shù)量,等級(jí)等動(dòng)態(tài)數(shù)據(jù),以json文件的方式保存,這里我使用了LitJson來實(shí)現(xiàn)。
首先寫個(gè)物品的基礎(chǔ)類,再在表格中填上對(duì)應(yīng)的靜態(tài)數(shù)據(jù):
public class ItemInfo
{
??? //動(dòng)態(tài)數(shù)據(jù),從json中獲取
??? public int Id;
??? public int amount;
??? public ItemQuality itemQuality;
??? public bool isSelected;
?
?
??? //靜態(tài)數(shù)據(jù)
??? public string name;
??? public string description;
??? public string []icons;//表格中圖片名字的集合
??? public ItemType itemType;
??? public Tool tool;
??? public int growTime;
?
?
??? /// 構(gòu)造函數(shù)僅構(gòu)造靜態(tài)數(shù)據(jù),通過表格id獲取
??? public ItemInfo(int BaseDataid )
??? {
??????? this.name = GetBaseData.instance.baseInfo[BaseDataid].name ;
??????? this.description = GetBaseData.instance.baseInfo[BaseDataid].description;
??????? this.icons = GetBaseData.instance.baseInfo[BaseDataid].icons;
??????? this.itemType = GetBaseData.instance.baseInfo[BaseDataid].itemType;
??????? this.tool = GetBaseData.instance.baseInfo[BaseDataid].tool;
??????? this.growTime = GetBaseData.instance.baseInfo[BaseDataid].growTime ;
??? }
}
?

表格的讀取我使用EPPlus插件,使用方法很簡(jiǎn)單,網(wǎng)上找到對(duì)應(yīng)dll文件到工程,調(diào)用這兩個(gè)指令集:
using OfficeOpenXml;
using System.IO;
?
靜態(tài)數(shù)據(jù)讀?。?/p>
??? public void GetExcelData()
??? {
??????? string FilePath = Application.dataPath + "/Resources/ItemData/BaseData.xlsx";
??????? Debug.Log("路徑" + FilePath);
??????? FileInfo fileInfo = new FileInfo(FilePath);
??????? if (fileInfo.Length == 0)
??????? {
??????????? Debug.Log("Excel文件不存在");
???????? ???return;
??????? }
?
??????? using (ExcelPackage excel = new ExcelPackage(fileInfo))
??????? {
??????????? ExcelWorksheet worksheet = excel.Workbook.Worksheets[1];
?
??????????? int maxRow_Data = worksheet.Dimension.End.Row; //行
??????????? int maxColumn_Data = worksheet.Dimension.End.Column; //列
??????????? int Id = 0;
?
??????????? //從第二行開始讀取數(shù)據(jù)
??????????? for (int i = 2; i <= maxRow_Data; i++)
??????????? {
??????????????? BaseMessage info? = new BaseMessage();
??????????????? string name=null;
??????????????? for (int j = 1; j <= maxColumn_Data; j++)
??????????????? {
??????????????????? switch (j)
??????????????????? {
??????????????????????? case 1:
??????????????????????????? info.name = worksheet.Cells[i, j].Value.ToString();
??????????????????????????? Debug.Log("名字" + name);
??????????????????????????? break;
??????????????????????? case 2:
??????????????????????????? info.description = worksheet.Cells[i, j].Value.ToString();
??????????????????????????? break;
??????????? ????????????case 3:
??????????????????????????? string temp = worksheet.Cells[i, j].Value.ToString();
??????????????????????????? info.icons = temp.Split(',');
??????????????????????????? break;
??????????????????????? case 4:
??????????????????????????? string temp2 = worksheet.Cells[i, j].Value.ToString();
??????????????????????????? info.itemType = (ItemType)int.Parse(temp2);
??????????????????????????? break;
??????????????????????? case 5:
??????????????????????????? string temp5 = worksheet.Cells[i, j].Value.ToString();
??????????????????????????? info.tool = (Tool)int.Parse(temp5);
??????????????????????????? break;
??????????????????????? case 6:
??????????????????????????? string temp6 = worksheet.Cells[i, j].Value.ToString();
???????????????????? ???????info.growTime= int.Parse(temp6);
??????????????????????????? break;
??????????????????????? default:
??????????????????????????? break;
??????????????????? }
??????????????? }
??????????????? baseInfo.Add(Id , info);
??????????????? Id++;
????????? ??}
??????? }//關(guān)閉表格
??? }
?
物品的Sprite單獨(dú)寫了個(gè)方法,方便直接通過名字讀?。?/p>
??? public string SpritesPath = "Sprites";
??? private Sprite[] sprites;
??? private Dictionary<string, object> spritesDictionary = new Dictionary<string, object>();
??? // 加載所有精靈圖
??? public void LoadAllSprites()
??? {
??????? sprites = Resources.LoadAll<Sprite>(SpritesPath);
??????? if (sprites.Length == 0)
??????? {
??????????? Debug.Log("精靈圖沒讀到");
??????? }
??????? for (int i = 0; i < sprites.Length; i++)
??????? {
??????????? spritesDictionary.Add(sprites[i].name, sprites[i]);
??????? }
??? }
?
??? // 直接通過名字獲得sprite
??? public Sprite ReadSpritesByString (string name)
??? {
??????? if (name == null)
??????????? Debug.Log("名字為空不存在");
??????? Sprite a = null;
??????? foreach (KeyValuePair<string, object> pair in spritesDictionary)
??????? {
??????????? // Debug.Log(pair.Key + " " + pair.Value);
??????????? if (pair.Key.ToString() == name)
??????????? {
????????????? a = pair.Value as Sprite;
??????????? }
??????? }
??????? return a;
??? }
?
然后新建一個(gè)動(dòng)態(tài)數(shù)據(jù)類和專門存放List<動(dòng)態(tài)數(shù)據(jù)>的類,方便json讀寫:
public class DynamicData
{
??? public int jsonId;//動(dòng)態(tài)數(shù)據(jù)ID
??? public int baseId;//對(duì)應(yīng)的靜態(tài)數(shù)據(jù)ID
??? public int amount;
??? public bool isSelected;
}
?
public class DynamicList
{
??? public List<DynamicData> dyDatas=new List<DynamicData>();
}
?
LitJson的使用方法和EPPlus一樣,導(dǎo)入文件后,調(diào)用這兩個(gè)指令集:
Json數(shù)據(jù)讀寫:
??? //存儲(chǔ)背包動(dòng)態(tài)數(shù)據(jù)
??? public void SaveBagData()
??? {
??????? string json = JsonMapper.ToJson(Bagmanager.instance.dynamicList);
???????
??????? //路徑下的文件不存在就新建個(gè)
??????? if (!File.Exists(path))
??????????? File.Create(path ).Close();
???????
??????? //選擇覆蓋模式,每次寫入數(shù)據(jù)會(huì)清除上次數(shù)據(jù)
??????? using (StreamWriter sw = new StreamWriter(new FileStream(path , FileMode.Truncate)))
??????? {
??????????? sw.Write(json);
??????? }
??? }
?
??? //讀取背包數(shù)據(jù)
??? public void ReadBagData()
??? {
??????? //檢查文件是否存在
??????? if (!File.Exists(path))
??????????? return;
?
??????? using (StreamReader sr = new StreamReader(new FileStream(path, FileMode.Open)))
??????? {
??????????? string json = sr.ReadToEnd();
??????????? DynamicList tempList = new DynamicList();
??????????? tempList = JsonMapper.ToObject<DynamicList>(json);
??????????? Bagmanager.instance.dynamicList = tempList;
??????? }
??????? Bagmanager.instance.RefreshBagData();
??? }
?
完成了背包物品的動(dòng)靜態(tài)數(shù)據(jù)讀取后 ,還要讓數(shù)據(jù)到背包的UI面板上展示,這里就可以新建一個(gè)模板預(yù)制體了,同時(shí)這里再新建一個(gè)腳本負(fù)責(zé)在游戲開始時(shí),將相應(yīng)的數(shù)據(jù)填入到預(yù)制體,并在面板上生成,這里就不詳細(xì)說了:


物品系統(tǒng)和背包有個(gè)雛形了,接下來看看土地咋整,首先需要對(duì)每一塊土地單獨(dú)進(jìn)行操作,所以需要一個(gè)規(guī)律的網(wǎng)格狀的土地,這里我自然而然就想到了Tilemap(瓦片地圖)系統(tǒng),故名思意,就是用一塊一塊瓦片搭建起來的地圖,簡(jiǎn)直就是為星露谷量身而做的系統(tǒng)??!起飛!

然而實(shí)際上使用還是遇到了很多問題,開始我以為Tile類就是組成Tilemap的每個(gè)圖塊,但查看了定義才發(fā)現(xiàn)Tile本質(zhì)還是一個(gè)繼承了ScriptableObject的,類似 unity 材質(zhì)或紋理資源的文件。(使用過Tilemap的朋友知道,使用前的第一步就是在Tile palette中導(dǎo)入Sprite生成Tile文件)所以直接對(duì)單個(gè)Tile文件操作,進(jìn)而改變游戲中的圖塊是不可取的,本末倒置了。
好在Unity還是很貼心的提供了方法來對(duì)每個(gè)單元格進(jìn)行操作,你可以獲取Tilemap的每個(gè)圖塊的坐標(biāo),然后進(jìn)行Tile的更換:

這里對(duì)Tilemap不熟的朋友可能有點(diǎn)懵,你可以瞅瞅這個(gè)視頻來了解下這個(gè)功能,賊好用:
簡(jiǎn)明UNITY教程】TileMap瓦片地圖的詳細(xì)使用方法
接下來開始實(shí)現(xiàn)土地,這里我創(chuàng)建了兩層Tilemap,第一層展示土地狀態(tài)(比如澆水土地會(huì)濕一塊),第二層展示物品,每次使用對(duì)應(yīng)的Tile都要手動(dòng)將sprite拖到Tile palette中生成還是挺麻煩的,這里同樣寫了個(gè)根據(jù)Sprite名生成Tile 的方法:

? //判定在Tilemap文件夾下否存在該名稱的tile文件,不存在就創(chuàng)建新的并保存下來
?
??? public Tile CheckTileExits(string name)
??? {
??????? string tempPath = string.Format("{0}{1}", "Tilemap/", name);
??????? string Path = string.Format("{0}{1}{2}", "/Resources/Tilemap/", name, ".asset");
??????? string Lastpath = Application.dataPath + Path;
??????? if (!File.Exists(Lastpath))
??????? {
??????????? CreateExampleAsset(name);
??????????? Tile land1 = (Tile)Resources.Load(tempPath);
??????????? return land1;
??????? }
??????? Tile land2 = (Tile)Resources.Load(tempPath);
??? ????return land2;
??? }
?
??? // 根據(jù)精靈圖名字創(chuàng)建tile
??? public void CreateExampleAsset(string name)
??? {
??????? string Path = "Assets/Resources/Tilemap/";
??????? Tile exampleAsset = Tile.CreateInstance<Tile>();
??????? exampleAsset.sprite = Bagmanager.instance.ReadSpritesByString(name);
??????? string temp = string.Format("{0}{1}{2}", Path, name, ".asset");
??????? AssetDatabase.CreateAsset(exampleAsset, temp);
??????? AssetDatabase.Refresh();
??? }
?
然后新建一個(gè)土地類:
public enum LandType
{
??? Unkown, normal, reclaimed, seeded, watered, planted
}
?
public class Land
{
??? public int Id;
??
??? //當(dāng)前土地狀態(tài)
??? public LandType landType;
?
??? //每一塊土地的坐標(biāo)(對(duì)應(yīng)Tilemap中的圖塊坐標(biāo))
??? public Vector3Int LandPos;
?
??? //需要展示的圖片集合
??? public string [] icons ;
?
??? //種子所需的成長(zhǎng)時(shí)間
??? public int growTime;
?
??? //種子一開始種下的時(shí)間
??? public int startTime;
?
??? //種子實(shí)際成長(zhǎng)時(shí)間
??? public float actualTime;
}
?
土地?cái)?shù)據(jù)的讀寫操作和物品的差不多,這里也不過多贅述,接下來解決操作問題,這里先不管動(dòng)畫,解決主要問題(其實(shí)就是懶),星露谷中允許對(duì)人物周圍的8個(gè)圖塊進(jìn)行各種操作,所以我們需要人物自身的坐標(biāo),土地已經(jīng)網(wǎng)格化了,所以人物自身的坐標(biāo)也需要轉(zhuǎn)換土地對(duì)應(yīng)的坐標(biāo)(這里要以人物的腳為中心點(diǎn)):
public Vector3Int[] GetHumanAround()
??? {
??????? Vector3Int temp = GetCurPos();
??????? Vector3Int[] temps = new Vector3Int[8];
??????? temps[0] = new Vector3Int(temp.x-1, temp.y+1,0);
??????? temps[1] = new Vector3Int(temp.x, temp.y+1,0);
??????? temps[2] = new Vector3Int(temp.x+1, temp.y+1,0);
??????? temps[3] = new Vector3Int(temp.x-1, temp.y,0);
???????
??????? temps[4] = new Vector3Int(temp.x+1, temp.y,0);
??????? temps[5] = new Vector3Int(temp.x-1, temp.y-1,0);
??????? temps[6] = new Vector3Int(temp.x, temp.y-1,0);
??????? temps[7] = new Vector3Int(temp.x + 1, temp.y - 1, 0);
??????? return temps;
??????
??? }
?
同時(shí)鼠標(biāo)坐標(biāo)也轉(zhuǎn)換為Int型,在指定圖塊進(jìn)行操作,滿足條件即可執(zhí)行~
最后值得一提的是時(shí)間和每個(gè)圖塊被人物碰到的小動(dòng)畫,這兩個(gè)部分我都用協(xié)程完成,雖然用協(xié)程記錄時(shí)間會(huì)有誤差,但星露谷并不是對(duì)時(shí)間要求很精確的游戲:
時(shí)間:
? void Start()
??? {
??????? StartCoroutine(TimeIncrease());
??? }
?
// 10s記錄一次時(shí)間,也順便刷新右上角的時(shí)間面板(10s相當(dāng)于星露谷的10分鐘)
? public IEnumerator TimeIncrease()
??? {
??????? while (true)
??????? {
??????????? while (gameTime.time <= 1200)
??????????? {
??????????????? gameTime.time += 10f;
??????????????? rotate.z = rotate.z + 0.15f;
?
??????????????? yield return new WaitForSeconds(10f);
??????????????? RotatePoint.Rotate(rotate);
??????????????? dayText.text = string.Format("{0}{1}", gameTime.day.ToString(), "日");
??????????????? hour = (int)(gameTime.time / 60);
??????????????? minute = (int)(gameTime.time - hour * 60);
??????????????? string tempTime = string.Format("{0}{1}{2}", hour.ToString(), ":", minute.ToString());
??????????????? minuteText.text = tempTime;
???????????? }
??????????? DayEnd();
??????? }
?}
?
每個(gè)圖塊的動(dòng)畫這里有點(diǎn)麻煩,Unity將每個(gè)圖塊的transform數(shù)據(jù)封裝到了一個(gè)4x4的矩陣中嗎,所以需要額外做一次矩陣轉(zhuǎn)換,再調(diào)用這個(gè)方法完成這個(gè)小動(dòng)畫:

??? //判斷是否碰到了種子
??? public void TouchCrop()
??? {
??????? Vector3Int tempPos1 = GetCurPos();
??????? Matrix4x4 curTrans=new Matrix4x4();
??????? foreach (var key in LandManager.instance.seededPos.Keys)
??????? {
??????????? if (LandManager.instance.seededPos[key] == tempPos1)
??????????? {
??????????????? curTrans = LandPanel.level2.GetTransformMatrix(LandManager.instance.lands.land2[key].LandPos);
?????????????? if (direction == Direction.Left || direction == Direction.DownLeft || direction == Direction.UpLeft)
??????????????? {
?????????????????? StartCoroutine(LeftShake(0.5f, curTrans, key));
????????? ??????}
??????????????? else
??????????????? {
?????????????????? StartCoroutine(RightShake(0.5f, curTrans, key));
??????????????? }
??????????????? break;
??????????? }
??????? }
??? }
?
??? // 莊稼左抖動(dòng)
??? public IEnumerator? LeftShake(float duration,Matrix4x4 curTrans,int key)
??? {
??????? Vector3 temp = new Vector3(0, 0, 0);
??????? float elapsed = 0f;
?
??????? while (elapsed < duration)
??????????? {
??????????????? if (elapsed < duration / 2)
??????????????? {
??????????????????? temp = temp + (leftAngel / duration) * Time.deltaTime * 2;
??????????????????? Quaternion temp_rotation = Quaternion.Euler(temp);
??????????????????? curTrans.SetTRS(curTrans.GetPostion(), temp_rotation, new Vector3(1, 1, 1));
??????????????????? LandPanel.level2.SetTransformMatrix(LandManager.instance.lands.land2[key].LandPos, curTrans);
??????????????? }
??????????????? else if(elapsed < duration)
??????????????? {
??????????????????? temp= temp- (leftAngel / duration) * Time.deltaTime * 2;
????????????? ??????Quaternion temp_rotation = Quaternion.Euler(temp);
??????????????????? curTrans.SetTRS(curTrans.GetPostion(), temp_rotation, new Vector3(1, 1, 1));
??????????????????? LandPanel.level2.SetTransformMatrix(LandManager.instance.lands.land2[key].LandPos, curTrans);
??????????????? }
??????????? elapsed += Time.deltaTime;
??????????? yield return 0;
??????? }
??? }
?
最后展示下目前的施工進(jìn)度,星露谷還未完結(jié),暫時(shí)先不放這個(gè)還未圓滿的工程了~

最后感謝知乎? @絮醬醬的EPPlus的小教程,永遠(yuǎn)滴神:馬三小伙兒寫的Litjson擴(kuò)展(Litjson原版不支持Vector3Int類型數(shù)據(jù))和_阿松先生的矩陣轉(zhuǎn)換工具類,感謝大佬們的工作,讓俺節(jié)省了很多的功夫~
貼上馬三小伙兒和阿松先生的原帖地址:
【Unity游戲開發(fā)】跟著馬三一起魔改LitJson
https://cloud.tencent.com/developer/article/1608178
【Unity矩陣知識(shí)】之Matrix4x4矩陣變換詳細(xì)實(shí)例
https://blog.csdn.net/aaa583004321/article/details/81948780?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param


歡迎加入游戲開發(fā)群歡樂攪基:1082025059
對(duì)游戲開發(fā)感興趣的童鞋可戳這里進(jìn)一步了解:http://www.levelpp.com/
我們的公眾號(hào):“皮皮關(guān)"
B站:“皮皮關(guān)做游戲”
用Unity還原星露谷物語(yǔ)(2):種地篇(數(shù)據(jù)讀寫+TileMap)的評(píng)論 (共 條)
