* 完整代码传送门
此次用到的Unity插件地址Unity3dAsyncAwaitUtil GitHub,如果使用async-await时仍然报错,请把.net standard 2.0转换为.net 4.x。
这里的报错其实很玄学,如果不转换的话只有编译器是报错的,Unity里实际运行一点问题都没有,为了好看还是换了吧,拓展库更多避免以后的问题,比如之前做到的自定义属性面板就必须得转换,不然命名空间类名都找不到。
本来是个2D的项目但是懒得新建工程,正好手头有个打开的3D项目随手就做了,因为里面还有别的代码和模型,所以这次就不发工程了。话不多说直接就开始吧。
用UGUI做的话需要先放置一个Panel在场景里,毕竟UI都是以Panel为单位的,这里自动生成的Canvas,因为场景只有这一个Canvas,所以直接把渲染模式设置为渲染相机,调整分辨率只调整相机就足够了。在Panel下创建一个背景层,再创建一个SumArea存放所有的Cell,然后就可以做Area的预制体了。
这里我为了方便计算做好的宽高都是100,自己可以根据情况来调整,因为没有合适的图片所以直接偷懒把原生的BackGround拿来用了,导致了后面分辨率太高有锯齿的情况没法解决,所以尽量找个背景拿来用吧,网上的白底好扣图,PS魔棒一扣就出来了,或者色调分离也可以完美解决,扯远了。首先背景解决了,数字的话想做的美观点可以用TextMeshPro,我就用的Text,Text设置为Area的子对象,size和pos的话尽量离背景的圈远一点,不然有时候会重叠。左右上下都设为居中,勾上best fit选项可以自动调整大小,可以随便打几个数字试下,做完这些保存预制体即可。
然后就是游戏结束和分数的UI,这几个也用Text做,和上面的大同小异,分数的话水平轴上可以设置为overflow,这样就不用担心大小不够而不显示的问题了,其他的也没什么需要注意的地方,锚点设置好就行,记得都要放在Panel的下面好管理。
上面的Main可以忽视掉,因为我用到了我其他脚本的代码需要初始化。
接下来就可以写脚本了,上面已经确定好当前的网格大小是100*100的单位,然后再确定棋盘格的长宽,用个100*100的Image比量一下就好。
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/748116106b4c6f4725e3aa51181682b0.png#pic_center)
//直接定义好三个变量显示在面板上设置好
[SerializeField] private Vector2 upperRight;
[SerializeField] private Vector2 lowerLeft;
[SerializeField] private float areaSize;
首先思索一下游戏逻辑,移动时分为行变换和列变换,所以需要两个列表来存引用,x方向的列表变换的话y方向也会跟着变换,所以列表内存引用类型的变量会变得简单一点,创建一个CellData类,使用元组来存储,或者拆分两个列表都行。同时再定义长宽能容纳几个Cell的变量。
public sealed class Viewer : MonoBehaviour
{
[SerializeField] private Vector2 upperRight;
[SerializeField] private Vector2 lowerLeft;
[SerializeField] private float areaSize;
private (List<CellData> x, List<CellData> y) _allPos;
private ushort _xSize;
private ushort _ySize;
}
public sealed class CellData
{
}
CellData代表着场景中每个方块单元,所以共有的属性就是位置和当前位置是否有图片这两个,是否有图片则需要图片的引用,所以图片也写进来。
public sealed class CellData
{
internal Image Image {
get; set; }
//每个位置都是固定的所以在构造方法里赋值就行
internal readonly Vector3 cellPos;
internal bool IsHaveImage => Image != null;
internal CellData(Vector3 cellPos)
{
this.cellPos = cellPos;
}
}
这些做完以后可以开始做初始化列表了。
/// <summary>
/// 初始化地图且生成x,y正方向的元组
/// </summary>
private void InitializeMap()
{
//设置一个临时位置便于修改后储存,不需要每次都new
var tempPos = Vector2.zero;
_xSize = Convert.ToUInt16((upperRight.x - lowerLeft.x) / areaSize + 1);
_ySize = Convert.ToUInt16((upperRight.y - lowerLeft.y) / areaSize + 1);
var xDic = new List<CellData>(Convert.ToInt32(_xSize*_ySize) + 1);
//向元组的x列表中添加引用
for (var i = lowerLeft.y; i <= upperRight.y; i += areaSize)
{
for (var j = lowerLeft.x; j <= upperRight.x; j += areaSize)
{
tempPos.Set(j, i);
xDic.Add(new CellData(tempPos));
}
}
//y列表使用x列表中的引用,方便后面代码编写,需要修改cellData的属性字段时xy只改一个就行
var yDic = new List<CellData>(Convert.ToInt32(_xSize*_ySize) + 1);
var count = 0;
for (var i = 0; i < xDic.Count + _xSize; i += _ySize)
{
if (i >= xDic.Count)
{
count++;
i = i - xDic.Count + 1;
}
if (count == 7)
{
break;
}
yDic.Add(xDic[i]);
}
_allPos = (xDic, yDic);
}
确保添加的引用全部正确以后就可以在开始游戏时添加数字了,接下来看注释就行,没什么难的地方。
public sealed class Viewer : MonoBehaviour
{
...
/// <summary>
/// 为场景中添加新数字
/// </summary>
/// <param name="count">要添加几个数字</param>
private void AddNumber2Map(sbyte count)
{
var i = 0;
while (i < count)
{
//找出没有图片的单元并成组
var emptyCellData = _allPos.x.Where(cellData => !cellData.IsHaveImage).ToList();
//如果没有空闲的地方则不再生成
if (emptyCellData.Count==0)
{
break;
}
//随机选取空闲单元
var randomCell = emptyCellData[Random.Range(0, emptyCellData.Count)];
//在空闲单元实例化新数字
var newArea = Instantiate(area, randomCell.cellPos, Quaternion.identity, sumArea.transform);
//随机生成2或4
var num = (Random.value > 0.7f ? 4 : 2).ToString();
//调整该单元的参数
randomCell.SetValue(this, randomCell.cellPos, newArea, num);
i++;
}
}
/// <summary>
/// 根据位置查找到存储当前位置的cellData
/// </summary>
/// <param name="nowPos">给定的位置</param>
/// <returns></returns>
public CellData this[Vector2 nowPos] =>
_allPos.x.Find(cellData => Vector2.Distance(cellData.cellPos, nowPos) < 0.05f);
}
public sealed class CellData
{
...
internal Text Text => Image.GetComponentInChildren<Text>();
internal RectTransform Transform => Image.GetComponent<RectTransform>();
internal Vector3 AnchoredVector3
{
set => Image.GetComponent<RectTransform>().anchoredPosition3D = value;
get => Image.GetComponent<RectTransform>().anchoredPosition3D;
}
/// <summary>
/// 调整单元内的参数,在生成的时候调用
/// </summary>
/// <param name="viewer">使用CellData的viewer脚本实例</param>
/// <param name="anchoredPos">游戏物体的位置</param>
/// <param name="newImage">新创建的image</param>
/// <param name="num">图片的数字</param>
internal void SetValue(Viewer viewer, Vector3 anchoredPos, Image newImage, string num)
{
var xCell = viewer[cellPos];
xCell.Image = newImage;
xCell.AnchoredVector3 = anchoredPos;
xCell.Text.text = num;
}
}
直接在Viewer里Awake调用两个方法测试。
开始的话都好办,没有太复杂的逻辑,现在可以说下移动的方法了。
移动需要接受外界输入,可以直接拿Input.GetAxisRaw()直接用,也可以写Input.GetKey(),前面的代码少就用前面的来做。
public sealed class Viewer : MonoBehaviour
{
...
private void Update()
{
var h = Input.GetAxisRaw("Horizontal");
var v = Input.GetAxisRaw("Vertical"