撰写了文章 发布于 2021-08-01 21:18:40
【自学笔记】制作动森岛建规划器(六)
Waterfall的AutoTile
RIver的Tile规则和Cliff基本上是一样的,主要区别在于,River改变的是当前格子的河流状态,而Cliff改变的是当前格子的地形状态。
为了这点区别写一个新的类,感觉太麻烦了,相同的Tile规则目前也不会出现在其他地方,所以我就直接加一个Bool变量在外部控制这个Tile应该设置格子的什么数据。
但是River有一个特殊的状态,当建造在Cliff的直边的时候,生成的就应当是Waterfall,而它的Tile判定方式也是特殊的。
所以这里需要专门为Waterfall写一个独特的AutoTile脚本。
WaterfallTile大体结构和AutoTile基本相似,只是要额外判断CliffSide的朝向,并且将对应方向的两个角设置为正确的瀑布模型。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WaterfallTile : MonoBehaviour
{
public Mesh center, cornerLU, cornerRU, cornerLD, cornerRD, innerCornerLU, innerCornerRU, innerCornerLD, innerCornerRD, sideU, sideD, sideL, sideR,
fallD, fallDL, fallDR, fallL, fallLD, fallLU, fallR, fallRD, fallRU, fallU, fallUL, fallUR;
public LayerMask cliffLayer;
public LayerMask riverLayer;
public GameObject cliffMesh;
public GameObject water;
int fallDir;//0=朝上,1=朝右,2=朝下,3=朝左
ArrayList gridCell;
bool[] tileState = new bool[8];
bool[] riverState = new bool[8];
Collider[] riverCollider = new Collider[8];
void Start()
{
gridCell = GameObject.Find("LevelController").GetComponent<LevelController>().gridDataList[(int)transform.position.x, (int)transform.position.z];
gridCell[2] = 2;
SetTile();
ResetSurroundingTile();
//ResetSurroundingTile();
}
void Update()
{
}
bool CheckCliffTile(Vector3 direction)
{
Vector3 origin = gameObject.transform.position + new Vector3(0, 0.5f, 0);
RaycastHit hit;
Physics.Raycast(origin, direction, out hit, 1, cliffLayer);
if (hit.collider != null)
{
return true;
}
else
{
return false;
}
}
ArrayList CheckRiverTile(Vector3 direction)
{
ArrayList checkOutput = new ArrayList() { false, null };
Vector3 origin = gameObject.transform.position + new Vector3(0, 0.5f, 0);
RaycastHit hit;
Physics.Raycast(origin, direction, out hit, 1, riverLayer);
if (hit.collider != null)
{
checkOutput[0] = true;
checkOutput[1] = hit.collider;
}
return checkOutput;
}
public void SetTile()
{
CheckSurroundingTileState();
if(fallDir == 0)
{
SetRiverCorner("LD", riverState[5], riverState[7], riverState[6], cornerLD, sideD, sideL, innerCornerLD, center);
SetRiverCorner("RD", riverState[3], riverState[5], riverState[4], cornerRD, sideR, sideD, innerCornerRD, center);
SetFall("RU", "LU", riverState[3], riverState[7], fallUR, fallUL, fallU);
}
else if(fallDir == 1)
{
SetRiverCorner("LU", riverState[7], riverState[1], riverState[0], cornerLU, sideL, sideU, innerCornerLU, center);
SetRiverCorner("LD", riverState[5], riverState[7], riverState[6], cornerLD, sideD, sideL, innerCornerLD, center);
SetFall("RD", "RU", riverState[1], riverState[5], fallRD, fallRU, fallR);
water.transform.rotation = Quaternion.Euler(0, 90, 0);
}
else if (fallDir == 2)
{
SetRiverCorner("LU", riverState[7], riverState[1], riverState[0], cornerLU, sideL, sideU, innerCornerLU, center);
SetRiverCorner("RU", riverState[1], riverState[3], riverState[2], cornerRU, sideU, sideR, innerCornerRU, center);
SetFall("LD", "RD", riverState[7], riverState[3], fallDL, fallDR, fallD);
water.transform.rotation = Quaternion.Euler(0, 180, 0);
}
else if (fallDir == 3)
{
SetRiverCorner("RU", riverState[1], riverState[3], riverState[2], cornerRU, sideU, sideR, innerCornerRU, center);
SetRiverCorner("RD", riverState[3], riverState[5], riverState[4], cornerRD, sideR, sideD, innerCornerRD, center);
SetFall("LU", "LD", riverState[5], riverState[1], fallLU, fallLD, fallL);
water.transform.rotation = Quaternion.Euler(0, -90, 0);
}
}
void CheckSurroundingTileState()
{
//从左上开始,顺时针计算
tileState[0] = (bool)CheckCliffTile(new Vector3(1, 0, -1));//左上
tileState[1] = (bool)CheckCliffTile(new Vector3(0, 0, -1));//上
tileState[2] = (bool)CheckCliffTile(new Vector3(-1, 0, -1));//右上
tileState[3] = (bool)CheckCliffTile(new Vector3(-1, 0, 0));//右
tileState[4] = (bool)CheckCliffTile(new Vector3(-1, 0, 1));//右下
tileState[5] = (bool)CheckCliffTile(new Vector3(0, 0, 1));//下
tileState[6] = (bool)CheckCliffTile(new Vector3(1, 0, 1));//左下
tileState[7] = (bool)CheckCliffTile(new Vector3(1, 0, 0));//左
riverState[0] = (bool)CheckRiverTile(new Vector3(1, 0, -1))[0];//左上
riverState[1] = (bool)CheckRiverTile(new Vector3(0, 0, -1))[0];//上
riverState[2] = (bool)CheckRiverTile(new Vector3(-1, 0, -1))[0];//右上
riverState[3] = (bool)CheckRiverTile(new Vector3(-1, 0, 0))[0];//右
riverState[4] = (bool)CheckRiverTile(new Vector3(-1, 0, 1))[0];//右下
riverState[5] = (bool)CheckRiverTile(new Vector3(0, 0, 1))[0];//下
riverState[6] = (bool)CheckRiverTile(new Vector3(1, 0, 1))[0];//左下
riverState[7] = (bool)CheckRiverTile(new Vector3(1, 0, 0))[0];//左
riverCollider[0] = (Collider)CheckRiverTile(new Vector3(1, 0, -1))[1];//左上
riverCollider[1] = (Collider)CheckRiverTile(new Vector3(0, 0, -1))[1];//上
riverCollider[2] = (Collider)CheckRiverTile(new Vector3(-1, 0, -1))[1];//右上
riverCollider[3] = (Collider)CheckRiverTile(new Vector3(-1, 0, 0))[1];//右
riverCollider[4] = (Collider)CheckRiverTile(new Vector3(-1, 0, 1))[1];//右下
riverCollider[5] = (Collider)CheckRiverTile(new Vector3(0, 0, 1))[1];//下
riverCollider[6] = (Collider)CheckRiverTile(new Vector3(1, 0, 1))[1];//左下
riverCollider[7] = (Collider)CheckRiverTile(new Vector3(1, 0, 0))[1];//左
if (!tileState[1] && !riverState[1])
{
fallDir = 0;
}
else if(!tileState[3] && !riverState[3])
{
fallDir = 1;
}
else if (!tileState[5] && !riverState[5])
{
fallDir = 2;
}
else if (!tileState[7] && !riverState[7])
{
fallDir = 3;
}
}
void SetRiverCorner(string name, bool stateL, bool stateR, bool stateCorner, Mesh corner, Mesh sideL, Mesh sideR, Mesh innerCorner, Mesh center)
{
if (!stateL && !stateR)
{
transform.Find(name).GetComponent<MeshFilter>().mesh = corner;
}
else if (stateL && !stateR)
{
transform.Find(name).GetComponent<MeshFilter>().mesh = sideR;
}
else if (!stateL && stateR)
{
transform.Find(name).GetComponent<MeshFilter>().mesh = sideL;
}
else if (stateL && stateR)
{
if (stateCorner)
{
transform.Find(name).GetComponent<MeshFilter>().mesh = center;
}
else
{
transform.Find(name).GetComponent<MeshFilter>().mesh = innerCorner;
}
}
}
void SetFall(string fallNameL, string fallNameR, bool stateL, bool stateR, Mesh fallL, Mesh fallR, Mesh center)
{
if(!stateL && !stateR)
{
transform.Find(fallNameL).GetComponent<MeshFilter>().mesh = fallL;
transform.Find(fallNameR).GetComponent<MeshFilter>().mesh = fallR;
}
else if(stateL && !stateR)
{
transform.Find(fallNameL).GetComponent<MeshFilter>().mesh = center;
transform.Find(fallNameR).GetComponent<MeshFilter>().mesh = fallR;
}
else if(!stateL && stateR)
{
transform.Find(fallNameL).GetComponent<MeshFilter>().mesh = fallL;
transform.Find(fallNameR).GetComponent<MeshFilter>().mesh = center;
}
else if(stateL && stateR)
{
transform.Find(fallNameL).GetComponent<MeshFilter>().mesh = center;
transform.Find(fallNameR).GetComponent<MeshFilter>().mesh = center;
}
}
void ResetSurroundingTile()
{
if (riverCollider[0])
{
if(riverCollider[0].gameObject.name == "Waterfall(Clone)")
{
riverCollider[0].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[0].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[1])
{
if (riverCollider[1].gameObject.name == "Waterfall(Clone)")
{
riverCollider[1].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[1].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[2])
{
if (riverCollider[2].gameObject.name == "Waterfall(Clone)")
{
riverCollider[2].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[2].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[3])
{
if (riverCollider[3].gameObject.name == "Waterfall(Clone)")
{
riverCollider[3].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[3].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[4])
{
if (riverCollider[4].gameObject.name == "Waterfall(Clone)")
{
riverCollider[4].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[4].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[5])
{
if (riverCollider[5].gameObject.name == "Waterfall(Clone)")
{
riverCollider[5].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[5].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[6])
{
if (riverCollider[6].gameObject.name == "Waterfall(Clone)")
{
riverCollider[6].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[6].gameObject.GetComponent<AutoTile>().SetTile();
}
}
if (riverCollider[7])
{
if (riverCollider[7].gameObject.name == "Waterfall(Clone)")
{
riverCollider[7].gameObject.GetComponent<WaterfallTile>().SetTile();
}
else
{
riverCollider[7].gameObject.GetComponent<AutoTile>().SetTile();
}
}
}
public void RemoveSelf()
{
gameObject.layer = 0;
gridCell[2] = 0;
GameObject clone = Instantiate(cliffMesh, transform.position, Quaternion.identity);
ResetSurroundingTile();
Destroy(gameObject);
}
public void SetColor(Color color)
{
gameObject.transform.Find("LU").GetComponent<MeshRenderer>().material.SetColor("_Color", color);
gameObject.transform.Find("RU").GetComponent<MeshRenderer>().material.SetColor("_Color", color);
gameObject.transform.Find("LD").GetComponent<MeshRenderer>().material.SetColor("_Color", color);
gameObject.transform.Find("RD").GetComponent<MeshRenderer>().material.SetColor("_Color", color);
}
private void OnMouseEnter()
{
if ((GameObject.Find("LevelController").GetComponent<LevelController>().isRemoveCliff && GameObject.Find("CliffRemover(Clone)") && gameObject.layer == 9)
|| (GameObject.Find("LevelController").GetComponent<LevelController>().isRemoveRiver && GameObject.Find("RiverRemover(Clone)") && gameObject.layer == 10))
{
SetColor(Color.red);
}
}
private void OnMouseExit()
{
if ((GameObject.Find("LevelController").GetComponent<LevelController>().isRemoveCliff && GameObject.Find("CliffRemover(Clone)") && gameObject.layer == 9)
|| (GameObject.Find("LevelController").GetComponent<LevelController>().isRemoveRiver && GameObject.Find("RiverRemover(Clone)") && gameObject.layer == 10))
{
SetColor(Color.white);
}
}
}
因为加上River与Waterfall之后,形状检测的判断条件变得复杂了,原本的AutoTile里相应的部分代码也需要照着WaterfallTile的方式改一遍。
瀑布底部的河流
现在基本的建造都有了,但还有一些小问题。
可以看到,Waterfall的底部还是普通的地面,没能和下一层的河流正常衔接。
所以接下来还需要在建造Waterfall的时候把Waterfall下方一格也生成一个River。同时在拆除Waterfall的同时也要把下方一格的River也一起拆除。
这一块的生成和拆除就直接做在WaterfallTile里面,在建造的时候都获取下方格子的对象,然后用相同方式替换成River,拆除则相反。
void SetBelowTile(bool isBuild)
{
if (isBuild)
{
Collider belowTile = (Collider)CheckCliffTile(new Vector3(0, -1, 0))[1];
GameObject clone = Instantiate(riverMesh, transform.position - new Vector3(0, 1, 0), Quaternion.identity);
Destroy(belowTile.gameObject);
}
else
{
Collider belowTile = (Collider)CheckRiverTile(new Vector3(0, -1, 0))[1];
if ((int)gridCell[0] > 1)
{
GameObject clone = Instantiate(cliffMesh, transform.position - new Vector3(0, 1, 0), Quaternion.identity);
}
else
{
GameObject clone = Instantiate(floorMesh, transform.position - new Vector3(0, 1, 0), Quaternion.identity);
}
belowTile.GetComponent<AutoTile>().ResetSurroundingTile();
Destroy(belowTile.gameObject);
}
}
这个函数在建造和拆除Waterfall的时候都执行一次,同时输入一个bool变量来控制是建造行为还是拆除行为
但这里暴露了一个BUG,因为在拆除River或是Waterfall的时候,会把之前销毁的Cliff重新生成回来,但是生成Cliff时会先将这个格子的「高度」+1,就会导致一个格子反复建造拆除河流之后,格子的「高度」持续变高而不符合实际高度。
可能可以把「高度」数据变化的计算放在Generator里,不过这个改动就比较大,所以我的解决办法就是在恢复Cliff的时候先把「高度」-1,这样高度就是正确的了。
有问题了以后再说嘛~~~
跟着又发现了一个很迷惑的BUG,单独一个Waterfall建造下去的时候,非常正常;而紧挨着一个Waterfall旁边跟着建造第二个Waterfall的时候,就会报一个错。。。。
报错的代码是这一段的belowTile的结果null。
Collider belowTile = (Collider)CheckCliffTile(new Vector3(0, -1, 0))[1]; Destroy(belowTile.gameObject);
只要不挨着建造就没事,一旦挨着建造了就变成null,简直太奇怪了。。。。
检查了半天也没发现问题在哪,它就是单纯的在检测取值的时候没能取到,就变成null了。最后随缘做了一个尝试,结果发现问题解决了,我感觉这个BUG更离谱了。。。。
解决办法是这样的,CheckCliffTile这个函数中有一个Raycast,我给它设置了一个最大检测距离为1,这行代码长这样:
Physics.Raycast(origin, direction, out hit, 1, cliffLayer);
然后我把这行代码的这个「1」删除了,也就是不限制检测距离,于是,这个问题就解决了。
就解决了。。。。
哪有这样的啊!!!明明有检测到为什么跟检测距离有关!为什么检测到了但没完全检测到!
(叹气。。)
你以为这问题就完了吗?不可能!!!
我尝试在第二层Cliff建造Waterfall时发现,当我紧挨着前一个Waterfall放下第二个的时候,除了报错以外,Hierarchy中本来应该只有2个的River此时的数量是3个,这个是在第一层建造时没有发生的情况。
但就是这个状况让我意识到问题出在哪儿了。
原因在于,我把SetBelowTile函数放到了SetTile函数的内部,以便在创建的同时完成替换下方Tile的工作。但问题在于,这个类中还有一个函数叫ResetSurroundingTile,它的作用是通过重新调用周围八格存在的Tile当中的SetTile函数,以便重适配它们的形状。
于是,因为SetBelowTile写到了SetTile内部,所以紧挨着的Waterfall会让它的邻居再执行一次SetTile并顺便也再执行了一次SetBelowTile,而这个时候它的邻居下方已经是River了,不属于检测范围内,所以射线检测才会返回null。
所以这个BUG必须紧挨着建造第二个Waterfall才会触发,因为只有紧挨着建造才会触发两次SetBelowTile函数,并且报错的也不是当时放下的那个Waterfall而是之前放下的那个。
至于为什么取消检测距离限制就不是null了,以及为什么在第一层建造不会多出那一个River,我就不清楚了。。。。
所以最后的解决办法,就是把建造时的SetBelowTile函数挪到Start下,在创建时执行一次就完了。
完善建造与拆除规则
现在除了对Cliff和River的转角进行倒角和直角的切换以外,其他的地形建造功能都已经完成,现在还有几个建造与拆除的BUG需要解决,将整个建造规则完善。
首先是河流周围一格的Cliff不能拆除,这个在拆除的时候对拆除对象的周围进行一次检测判断,如果周围8格没有河流的话才允许拆除。
这部分还有反馈效果需要做,否则除我以外的人是无法知道为什么不能拆除的,以及有正确反馈对于找BUG也是有好处的。但这个反馈效果可以在之后再来做,现在可以先忽略。
首先在AutoTile里面增加一组检测函数,用来判断周围的River存在情况。
bool CheckRiver(Vector3 direction)
{
Vector3 origin = gameObject.transform.position + new Vector3(0, 0.5f, 0);
RaycastHit hit;
Physics.Raycast(origin, direction, out hit, 1, riverLayer);
if (hit.collider != null)
{
return true;
}
else
{
return false;
}
}
public bool CheckSurroundingRiverState()
{
riverState[0] = (bool)CheckRiver(new Vector3(1, 0, -1));//左上
riverState[1] = (bool)CheckRiver(new Vector3(0, 0, -1));//上
riverState[2] = (bool)CheckRiver(new Vector3(-1, 0, -1));//右上
riverState[3] = (bool)CheckRiver(new Vector3(-1, 0, 0));//右
riverState[4] = (bool)CheckRiver(new Vector3(-1, 0, 1));//右下
riverState[5] = (bool)CheckRiver(new Vector3(0, 0, 1));//下
riverState[6] = (bool)CheckRiver(new Vector3(1, 0, 1));//左下
riverState[7] = (bool)CheckRiver(new Vector3(1, 0, 0));//左
for(int i =0; i < riverState.Length; i++)
{
if(riverState[i])
{
return true;
}
}
return false;
}
然后在TileRemover中增加一个判断条件,在拆除Cliff的时候,如果这个Cliff周围不存在River,才允许拆除。
else if(cursorPoint.collider.gameObject.name == "Cliff(Clone)")
{
if(!cursorPoint.collider.gameObject.GetComponent<AutoTile>().CheckSurroundingRiverState())
{
cursorPoint.collider.gameObject.GetComponent<AutoTile>().RemoveSelf();
}
}
接着是上方或周围8格的上方有其他Cliff的Tile不能拆除。
这个可以通过以高一层的位置为原点进行射线检测来实现,这里就需要先对射线检测的函数做一些调整,把原点变成一个输入的参数。
ArrayList CheckTile(float originHeight, Vector3 direction)
{
ArrayList checkOutput = new ArrayList() { false, null };
Vector3 origin = gameObject.transform.position + new Vector3(0, originHeight, 0);
RaycastHit hit;
Physics.Raycast(origin, direction, out hit, 1, layer);
if (hit.collider != null)
{
checkOutput[0] = true;
checkOutput[1] = hit.collider;
}
return checkOutput;
}
接下来就跟前面检测是否有河一样了。
最后是Waterfall前方一格不能建造Cliff,这次不再是拆除规则,而是建造规则,所以这部分就只能写在Generator里了。
检测的部分和AutoTile完全一样,所以直接抄过来就好,唯一区别在于这里只需要判断正方向,而不需要判断斜方向。
bool CheckSurroundingTileState()
{
//从左上开始,顺时针计算
tileState[0] = (bool)CheckTile(new Vector3(0, 0, -1))[0];//上
tileState[1] = (bool)CheckTile(new Vector3(-1, 0, 0))[0];//右
tileState[2] = (bool)CheckTile(new Vector3(0, 0, 1))[0];//下
tileState[3] = (bool)CheckTile(new Vector3(1, 0, 0))[0];//左
for (int i = 0; i < tileState.Length; i++)
{
if (tileState[i])
{
return true;
}
}
return false;
}
然后再在建造的函数里加上这个判断条件,就完成了。
到这里为止,除了倒角以外的主要地形建造功能以及完成,整体效果如下:
目录