大家好,我是想对🐎🏇使用炎拳的炎拳,上一篇星露谷中我完成了人物的移动并搭建了一个简单的界面,这篇就来瞅瞅星露谷的地是怎么种的。
首先看看游戏中的种地的过程:
大致上完成一次耕种,需要这几个步骤:选中锄头—开垦一块地—选择种子种下—浇水;同时种子会随时间的流逝成长,长到有叶片后人物走过,会触发一个庄稼动一下的动画。
所以这里我要做的功能就清晰了,先整个庄稼的预制体,其中包含所需的图片文字素材,然后点击一下土地就在那位置生成一个,大功告成啦!
才怪~
考虑到星露谷的物品繁多,如果用预制体来制作每个单独的物品,就太笨拙了;同时每个物品有自己的描述和各种属性,对应的Sprites也可以通过其地址实时读取,所以我希望能通过表格来填写这些物品的固有属性,这些数据属于静态数据。
同时种子,鱼,这些物品使用时会有数量的增减,而斧头,锄头这些道具则可以进行升级,数量,等级这些数据属于动态数据,经常改变,游戏结束也需要保存下来。
所以最后我的方案是:物品的名字,描述,Sprites地址等静态数据通过表格获得,表格的读取使用EPPlus插件实现;物品的数量,等级等动态数据,以json文件的方式保存,这里我使用了LitJson来实现。
首先写个物品的基础类,再在表格中填上对应的静态数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class ItemInfo { public int Id; public int amount; public ItemQuality itemQuality; public bool isSelected;
public string name; public string description; public string []icons; public ItemType itemType; public Tool tool; public int growTime;
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插件,使用方法很简单,网上找到对应dll文件到工程,调用这两个指令集:
1 2
| using OfficeOpenXml; using System.IO;
|
静态数据读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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;
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++; } } }
|
物品的Sprite单独写了个方法,方便直接通过名字读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 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]); } }
public Sprite ReadSpritesByString (string name) { if (name == null) Debug.Log("名字为空不存在"); Sprite a = null; foreach (KeyValuePair<string, object> pair in spritesDictionary) { if (pair.Key.ToString() == name) { a = pair.Value as Sprite; } } return a; }
|
然后新建一个动态数据类和专门存放List<动态数据>的类,方便json读写:
1 2 3 4 5 6 7 8 9 10 11 12
| public class DynamicData { public int jsonId; public int baseId; public int amount; public bool isSelected; }
public class DynamicList { public List<DynamicData> dyDatas=new List<DynamicData>(); }
|
Json数据读写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| using LitJson; using System.IO;
public void SaveBagData() { string json = JsonMapper.ToJson(Bagmanager.instance.dynamicList); if (!File.Exists(path)) File.Create(path ).Close(); using (StreamWriter sw = new StreamWriter(new FileStream(path , FileMode.Truncate))) { sw.Write(json); } }
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(); }
|
完成了背包物品的动静态数据读取后 ,还要让数据到背包的UI面板上展示,这里就可以新建一个模板预制体了,同时这里再新建一个脚本负责在游戏开始时,将相应的数据填入到预制体,并在面板上生成,这里就不详细说了:
物品系统和背包有个雏形了,接下来看看土地咋整,首先需要对每一块土地单独进行操作,所以需要一个规律的网格状的土地,这里当然要使用Tilemap(瓦片地图)系统。
开始我以为Tile类就是组成Tilemap的每个图块,但查看了定义才发现Tile本质还是一个继承了ScriptableObject的,类似 unity 材质或纹理资源的文件。(使用过Tilemap的朋友知道,使用前的第一步就是在Tile palette中导入Sprite生成Tile文件)所以直接对单个Tile文件操作,进而改变游戏中的图块是不可取的,本末倒置了。
好在Unity还是很贴心的提供了方法来对每个单元格进行操作,你可以获取Tilemap的每个图块的坐标,然后进行Tile的更换:
接下来开始实现土地,这里我创建了两层Tilemap,第一层展示土地状态(比如浇水土地会湿一块),第二层展示物品,每次使用对应的Tile都要手动将sprite拖到Tile palette中生成还是挺麻烦的,这里同样写了个根据Sprite名生成Tile 的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
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; }
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(); }
|
然后新建一个土地类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public enum LandType { Unkown, normal, reclaimed, seeded, watered, planted }
public class Land { public int Id; public LandType landType;
public Vector3Int LandPos;
public string [] icons ;
public int growTime;
public int startTime;
public float actualTime; }
|
土地数据的读写操作和物品的差不多,这里也不过多赘述,接下来解决操作问题,这里先不管动画,解决主要问题(其实就是懒),星露谷中允许对人物周围的8个图块进行各种操作,所以我们需要人物自身的坐标,土地已经网格化了,所以人物自身的坐标也需要转换土地对应的坐标(这里要以人物的脚为中心点):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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; }
|
同时鼠标坐标也转换为Int型,在指定图块进行操作,满足条件即可执行.
最后值得一提的是时间和每个图块被人物碰到的小动画,这两个部分我都用协程完成,虽然用协程记录时间会有误差,但星露谷并不是对时间要求很精确的游戏:
时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
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(); } }
|
每个图块的动画这里有点麻烦,Unity将每个图块的transform数据封装到了一个4x4的矩阵中吗,所以需要额外做一次矩阵转换,再调用这个方法完成这个小动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| 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; } } }
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; } }
|
最后展示下目前的施工进度,星露谷还未完结,暂时先不放这个还未圆满的工程了:
最后感谢知乎 @絮酱酱 的EPPlus的小教程,永远滴神:马三小伙儿写的Litjson扩展(Litjson原版不支持Vector3Int类型数据)和_阿松先生的矩阵转换工具类,感谢大佬们的工作,让俺节省了很多的功夫~
贴上马三小伙儿和阿松先生的原帖地址:
魔改Json
Matrix4x4矩阵变换详细实例