一、前言
嗨,大家好,我是新发。 最近一直在忙一些事情,好几天没写文章了,前天收到CSDN 的中秋礼物通知,非常感谢,特地做个小Demo 感谢一下CSDN 。
二、运行效果
运行效果如下,方阵迎面跑来, CSDN 方阵, 阵型变换,
三、实现原理
实现原理很简单,画成图是这样子,
下面我来讲下具体的实现细节~
四、图片资源
准备两张图片,如下: 勾选Read/Write Enabled ,设置图片为可读,如下:
五、模型资源
准备一个模型资源,
带一个站立和跑步动画,
动画状态机如下,使用混合树(Blend Tree )来过渡站立和跑动画, 混合树内部如下,通过Speed 变量来控制混合: Speed 变量(Float 类型)如下: 混合设置如下:
六、像素采样,生成点阵
像素采样生成点阵的逻辑,我封装在TextureFormation 脚本中。
图片像素采样,我们可以使用Texture2D 的GetPixel 接口,
public Color GetPixel(int x, int y);
我们可以把一张图切割成很多个小正方块(小方块的边长为samplingStep ),比如像这样子, 对每个小方块进行逐像素采样,我们知道,一个像素的颜色是由RGBA 四个通道值来表示的,每个通道的取值范围是0~255 , 对应到Color 这个类,就是r 、g 、b 、a , 这里要注意,Color 的rgba 是归一化的,也就是取值范围是0~1 ,如果想用0~255 的取值范围表示颜色,则对应的类是Color32 。 GetPixel 接口返回的是Color 对象,我们可以通过rgb 来简单判断一个像素是否有颜色,例:
if (color.r + color.g + color.b > 1f)
{
}
如果一个方块中有颜色的像素超过了方块的边长samplingStep ,则认为这个小方块中央需要安排一个人,否则留空。 我们声明一个数组来存放点阵数据:
private List<Vector3> posList = new List<Vector3>();
生成点阵的逻辑如下:
public int samplingStep = 5;
public float scale = 1f;
public Texture2D texture;
private void CalculatePoints()
{
if(Application.isPlaying)
{
if (0 != posList.Count)
return;
}
else
{
posList.Clear();
}
var widthStep = texture.width / samplingStep;
var heightStep = texture.height / samplingStep;
for (int i = 0; i <= heightStep; i += samplingStep)
{
for (int j = 0; j <= widthStep; j += samplingStep)
{
int colorPixelCnt = 0;
for (int ii = 0; ii <= samplingStep; ++ii)
{
for (int jj = 0; jj <= samplingStep; ++jj)
{
var color = texture.GetPixel(j * samplingStep + jj, i * samplingStep + ii);
if (color.r + color.g + color.b > 1f)
{
++colorPixelCnt;
}
}
}
if (colorPixelCnt > samplingStep)
{
var pos = new Vector3(-texture.width / 2 + j * samplingStep + samplingStep / 2f, 0, -texture.height / 2 + i * samplingStep + samplingStep / 2f);
pos *= scale;
posList.Add(pos);
}
}
}
}
我们再提供一个获取点阵数据的接口供外部调用:
public IEnumerable<Vector3> EvaluatePoints()
{
CalculatePoints();
var rootPos = Vector3.zero;
if (null != trans)
rootPos = trans.position;
for (int i = 0; i < posList.Count; ++i)
{
yield return rootPos + posList[i];
}
}
为了方便在编辑器下预览点阵,我们可以写个OnDrawGizmos() 方法,通过Gizmos 来绘制几何体,如下:
using UnityEngine;
public class FormationRenderer : MonoBehaviour
{
private TextureFormation _formation;
public TextureFormation Formation
{
get
{
if (_formation == null) _formation = GetComponent<TextureFormation>();
return _formation;
}
set => _formation = value;
}
[SerializeField] private Vector3 _unitGizmoSize;
[SerializeField] private Color _gizmoColor;
private void OnDrawGizmos()
{
if (Formation == null || Application.isPlaying) return;
Gizmos.color = _gizmoColor;
foreach (var pos in Formation.EvaluatePoints())
{
Gizmos.DrawCube(transform.position + pos + new Vector3(0, _unitGizmoSize.y * 0.5f, 0), _unitGizmoSize);
}
}
}
效果: 可以调节采样梯度和坐标缩放, 如下:
七、根据点阵生成角色方阵
我们创建一个Main.cs 脚本来实现这部分的逻辑。
有了点阵数据,我们就可以生成相应的角色啦,不过我们这里的每个角色都有各自的一些信息,比如动画、速度等,这里我们封装一个PlayerUnit 类来包装一下,
public class PlayerUnit
{
public GameObject obj;
public Transform trans;
public Animator ani;
public float speed;
}
封装一下生成角色和删除角色的接口,
private readonly List<PlayerUnit> spawnedUnits = new List<PlayerUnit>();
private void SpawnAvatar(IEnumerable<Vector3> points)
{
foreach (var pos in points)
{
var unit = new PlayerUnit();
var obj = Instantiate(unitPrefab, transform.position + pos, Quaternion.identity, parentTrans);
unit.obj = obj;
unit.trans = obj.transform;
unit.ani = obj.GetComponent<Animator>();
spawnedUnits.Add(unit);
}
}
private void DeleteAvatar(int num)
{
for (var i = 0; i < num; i++)
{
var unit = _spawnedUnits.Last();
spawnedUnits.Remove(unit);
Destroy(unit.obj);
}
}
根据点阵图生成角色,
private void GenFormation()
{
points = formation.EvaluatePoints().ToList();
if (points.Count > spawnedUnits.Count)
{
var remainingPoints = points.Skip(spawnedUnits.Count);
SpawnAvatar(remainingPoints);
}
else if (points.Count < spawnedUnits.Count)
{
DeleteAvatar(spawnedUnits.Count - points.Count);
}
for (var i = 0; i < spawnedUnits.Count; i++)
{
unit.trans.position = points[i];
}
}
此时的效果:
八、方阵行走
我们要让方阵跑起来,每个角色朝着自己的位置移动、旋转,并且配套播放跑步和站立的动画。 这里需要要让状态过渡比较自然,我是根据距离来决定动画混合,使用线性差值来计算旋转,代码如下: 代码如下:
for (var i = 0; i < spawnedUnits.Count; i++)
{
var unit = spawnedUnits[i];
var distance = Vector3.Distance(points[i], unit.trans.position);
if (distance > unitSpeed)
{
var dir = points[i] - unit.trans.position;
unit.trans.forward = Vector3.Lerp(unit.trans.forward, new Vector3(dir.x, 0, dir.z), 5 * Time.deltaTime);
unit.speed = distance > 0.8f ? distance : 0.8f;
unit.ani.SetFloat("Speed", unit.speed);
unit.trans.position = unit.trans.position + (points[i] - unit.trans.position).normalized * unitSpeed;
}
else
{
unit.trans.position = points[i];
if (unit.speed > 0)
{
unit.speed -= Time.deltaTime * 0.5f;
if (unit.speed < 0)
unit.speed = 0;
unit.ani.SetFloat("Speed", unit.speed);
}
unit.trans.forward = Vector3.Lerp(unit.trans.forward, -Vector3.forward, 5 * Time.deltaTime);
}
}
我们想点击地面时让整个方阵移动,这里我用了射线检测,
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if (Physics.Raycast(ray, out hitInfo, 200))
{
if ("ground" == hitInfo.collider.tag)
{
formation.transform.position = hitInfo.point;
}
}
}
其中,地面的tag 设置为ground , 效果如下: 如果你强行把某个角色拉到别处,她会自动乖乖跑回去站好,
九、方阵变换
我们想要实现多个方阵的变换,需要中途切换图片,并且可能需要设置对应的采样梯度和坐标缩放,我这里封装成可序列化的类,如下:
[System.Serializable]
public class TextureUnit
{
public Texture2D texture;
public int samplingStep;
public float scale;
}
声明一个public 的数组:
public TextureUnit[] textureUnits = new TextureUnit[0];
这样就可以在Inspector 面板中设置数据啦~ 写个方法实现方阵变换,
private void ChangeFormation()
{
if (curTextureIndex > (textureUnits.Length - 1))
{
curTextureIndex = 0;
}
var curTextureUnit = textureUnits[curTextureIndex];
formation.texture = curTextureUnit.texture;
formation.samplingStep = curTextureUnit.samplingStep;
formation.scale = curTextureUnit.scale;
formation.ReCalculate();
}
在Update 中检测空白键按下,如果按下则调用方阵变换,
private int curTextureIndex = 0;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
++curTextureIndex;
ChangeFormation();
}
}
效果如下:
十、工程源码
本工程我已上传到CODE CHINA ,感兴趣的同学可自行下载学习。 地址:https://codechina.csdn.net/linxinfa/UnityFormationsDemo 注:我使用的Unity 版本为Unity 2021.1.9f1c1 (64-bit) 。
十一、完毕
好了,就到这里吧, 我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~ 喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,我们下期见~
|