【游戏开发创新】使用Unity制作方阵编队,CSDN方阵迎面走来,感谢CSDN的中秋礼物(图像采样 | 点阵 | 方阵 | 队形 | 变换 | 动画) 刺骨的言语ヽ痛彻心扉 2022-09-08 11:53 272阅读 0赞 ### 文章目录 ### * * * 一、前言 * 二、运行效果 * 三、实现原理 * 四、图片资源 * 五、模型资源 * 六、像素采样,生成点阵 * 七、根据点阵生成角色方阵 * 八、方阵行走 * 九、方阵变换 * 十、工程源码 * 十一、完毕 ### 一、前言 ### 嗨,大家好,我是新发。 最近一直在忙一些事情,好几天没写文章了,前天收到`CSDN`的中秋礼物通知,非常感谢,特地做个小`Demo`感谢一下`CSDN`。 ### 二、运行效果 ### 运行效果如下,方阵迎面跑来, ![请添加图片描述][299d8560fca840808f2ff798c1c4c3e0.gif] `CSDN`方阵, ![请添加图片描述][ba0fc6ba368e479c981d36bde9becf60.gif] 阵型变换, ![请添加图片描述][761da053ad2e4d07994b8047aab5a51f.gif] ![请添加图片描述][99201cbdaf844d27a39114b0ebb9a1ed.gif] ### 三、实现原理 ### 实现原理很简单,画成图是这样子, ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16] 下面我来讲下具体的实现细节~ ### 四、图片资源 ### 准备两张图片,如下: ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 1] ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 2] 勾选`Read/Write Enabled`,设置图片为可读,如下: ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 3] ### 五、模型资源 ### 准备一个模型资源, ![请添加图片描述][18891415b7a542ef875fd07b8769f162.gif] 带一个站立和跑步动画, ![请添加图片描述][6301ab2a3e764004a3b3d17dbbcb5642.gif] ![请添加图片描述][adc042252e784cb5818c8b084b21ccd4.gif] 动画状态机如下,使用混合树(`Blend Tree`)来过渡站立和跑动画, ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_16_color_FFFFFF_t_70_g_se_x_16] 混合树内部如下,通过`Speed`变量来控制混合: ![请添加图片描述][d4e59dce4ec94abfbcbff0c838f3ce69.gif] `Speed`变量(`Float`类型)如下: ![在这里插入图片描述][ec0b4860055f4233a2a591b2de071e9a.png] 混合设置如下: ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_16_color_FFFFFF_t_70_g_se_x_16 1] ### 六、像素采样,生成点阵 ### 像素采样生成点阵的逻辑,我封装在`TextureFormation`脚本中。 图片像素采样,我们可以使用`Texture2D`的`GetPixel`接口, public Color GetPixel(int x, int y); 我们可以把一张图切割成很多个小正方块(小方块的边长为`samplingStep`),比如像这样子, ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 4] 对每个小方块进行逐像素采样,我们知道,一个像素的颜色是由`RGBA`四个通道值来表示的,每个通道的取值范围是`0~255`, ![在这里插入图片描述][817cca23de44450f811deec22e5be330.png] 对应到`Color`这个类,就是`r`、`g`、`b`、`a`, ![在这里插入图片描述][33864105c98c4c419c59bd675c316e68.png] 这里要注意,`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>(); 生成点阵的逻辑如下: // TextureFormation.cs /// <summary> /// 采样梯度,梯度越小,进度越高 /// </summary> public int samplingStep = 5; /// <summary> /// 坐标缩放 /// </summary> public float scale = 1f; /// <summary> /// 要采样的图片纹理 /// </summary> public Texture2D texture; // ... /// <summary> /// 计算点阵 /// </summary> 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) { // 一个block 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); } } } } 我们再提供一个获取点阵数据的接口供外部调用: // TextureFormation.cs /// <summary> /// 获取点阵数据 /// </summary> /// <returns></returns> 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`来绘制几何体,如下: // FormationRenderer.cs 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); } } } 效果: ![请添加图片描述][e7e522685d684f1c8a0ef4ddaad26774.gif] 可以调节采样梯度和坐标缩放, ![在这里插入图片描述][78d43555f8064c96a5cb60673708415f.png] 如下: ![请添加图片描述][870cc94ccdad4c46995166211b1a2c65.gif] ### 七、根据点阵生成角色方阵 ### 我们创建一个`Main.cs`脚本来实现这部分的逻辑。 有了点阵数据,我们就可以生成相应的角色啦,不过我们这里的每个角色都有各自的一些信息,比如动画、速度等,这里我们封装一个`PlayerUnit`类来包装一下, // Main.cs public class PlayerUnit { public GameObject obj; public Transform trans; public Animator ani; public float speed; } 封装一下生成角色和删除角色的接口, // Main.cs 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]; // TODO 移动、旋转、播动画 } } 此时的效果: ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 5] ### 八、方阵行走 ### 我们要让方阵跑起来,每个角色朝着自己的位置移动、旋转,并且配套播放跑步和站立的动画。 这里需要要让状态过渡比较自然,我是根据距离来决定动画混合,使用线性差值来计算旋转,代码如下: 代码如下: 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`, ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 6] 效果如下: ![请添加图片描述][80db7ec4d4014246b9471490a74ae14f.gif] 如果你强行把某个角色拉到别处,她会自动乖乖跑回去站好, ![请添加图片描述][be47be9be00b437192557ae6108d75db.gif] ### 九、方阵变换 ### 我们想要实现多个方阵的变换,需要中途切换图片,并且可能需要设置对应的采样梯度和坐标缩放,我这里封装成可序列化的类,如下: [System.Serializable] public class TextureUnit { public Texture2D texture; public int samplingStep; public float scale; } 声明一个`public`的数组: public TextureUnit[] textureUnits = new TextureUnit[0]; 这样就可以在`Inspector`面板中设置数据啦~ ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_17_color_FFFFFF_t_70_g_se_x_16] 写个方法实现方阵变换, // Main.cs // 方阵变换 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`中检测空白键按下,如果按下则调用方阵变换, // Main.cs private int curTextureIndex = 0; private void Update() { // ... if (Input.GetKeyDown(KeyCode.Space)) { ++curTextureIndex; ChangeFormation(); } } 效果如下: ![请添加图片描述][761da053ad2e4d07994b8047aab5a51f.gif] ### 十、工程源码 ### 本工程我已上传到`CODE CHINA`,感兴趣的同学可自行下载学习。 地址:[https://codechina.csdn.net/linxinfa/UnityFormationsDemo][https_codechina.csdn.net_linxinfa_UnityFormationsDemo] 注:我使用的`Unity`版本为`Unity 2021.1.9f1c1 (64-bit)`。 ![在这里插入图片描述][watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 7] ### 十一、完毕 ### 好了,就到这里吧, 我是林新发:[https://blog.csdn.net/linxinfa][https_blog.csdn.net_linxinfa] ![请添加图片描述][069f79386e58421f9e2b9ed8d9ae9c46.gif] 原创不易,若转载请注明出处,感谢大家~ 喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,我们下期见~ [299d8560fca840808f2ff798c1c4c3e0.gif]: /images/20220829/b19f912b486c46bf9aef41416738cf78.png [ba0fc6ba368e479c981d36bde9becf60.gif]: /images/20220829/89e6ad4b2fc340019ea8de9bc2792a85.png [761da053ad2e4d07994b8047aab5a51f.gif]: /images/20220829/b855e037d9dd43e19e8141ad5b83989e.png [99201cbdaf844d27a39114b0ebb9a1ed.gif]: /images/20220829/294207cb3df14d7cb441150ee0b6d0f9.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16]: /images/20220829/33a753bed2f143ddb3729feb1419bbb5.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 1]: /images/20220829/8c808497084b4debbd699c4bb5f78a4c.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 2]: /images/20220829/b504a317660c47c185fcfd940235144e.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 3]: /images/20220829/16eb02bd59e04737b2aae84696d874c3.png [18891415b7a542ef875fd07b8769f162.gif]: /images/20220829/10d758390f424bd1a5f5a6a7b32c54cd.png [6301ab2a3e764004a3b3d17dbbcb5642.gif]: /images/20220829/79b9b7b22d6d4a7597d003d144901ec4.png [adc042252e784cb5818c8b084b21ccd4.gif]: /images/20220829/329de909c89641138fe26a3be03f9947.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_16_color_FFFFFF_t_70_g_se_x_16]: /images/20220829/c7dd8c14df0c4b21b64c77a1886bfc36.png [d4e59dce4ec94abfbcbff0c838f3ce69.gif]: /images/20220829/b3aa48aef7014f1c839465c802b6383e.png [ec0b4860055f4233a2a591b2de071e9a.png]: /images/20220829/54db5e58b69d4fcc9def4302463b7d1e.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_16_color_FFFFFF_t_70_g_se_x_16 1]: /images/20220829/2b959012eb9b47abbcac2a0c4d818bc5.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 4]: /images/20220829/f7e33f56ffd148dcb8e4ac1451731ed1.png [817cca23de44450f811deec22e5be330.png]: /images/20220829/cbc5bfcdd0f94985a4e72769df189aae.png [33864105c98c4c419c59bd675c316e68.png]: /images/20220829/798042f009e8411aa3d5b57da3b700e8.png [e7e522685d684f1c8a0ef4ddaad26774.gif]: /images/20220829/e91cf8e7d61a4754933a66e7fa46b9e5.png [78d43555f8064c96a5cb60673708415f.png]: /images/20220829/c778901515da4ab0902637b1f8c1fb27.png [870cc94ccdad4c46995166211b1a2c65.gif]: /images/20220829/7738e9c03619400487434219d3a20887.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 5]: /images/20220829/a856b40a074b4289a0c86fee587efe39.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 6]: /images/20220829/6e38d4cd2b22498e859b6d4b7247796e.png [80db7ec4d4014246b9471490a74ae14f.gif]: /images/20220829/16eab7a25cbe4fe8b4b8412c4563b6ea.png [be47be9be00b437192557ae6108d75db.gif]: /images/20220829/4e83a7bc9d874a8dbe49c9fe06169564.png [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_17_color_FFFFFF_t_70_g_se_x_16]: /images/20220829/1181a3d621694daa915c7c973811500d.png [https_codechina.csdn.net_linxinfa_UnityFormationsDemo]: https://codechina.csdn.net/linxinfa/UnityFormationsDemo [watermark_type_ZHJvaWRzYW5zZmFsbGJhY2s_shadow_50_text_Q1NETiBA5p6X5paw5Y-R_size_20_color_FFFFFF_t_70_g_se_x_16 7]: /images/20220829/6134ce4bce31469585035bee6e57fe5d.png [https_blog.csdn.net_linxinfa]: https://blog.csdn.net/linxinfa [069f79386e58421f9e2b9ed8d9ae9c46.gif]: /images/20220829/770409634a874433baeafae25f656fb9.png
相关 旋转方阵 ![watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1NpbW9u 旧城等待,/ 2023年07月15日 12:48/ 0 赞/ 121 阅读
相关 有意思的方阵变换 Description 由0和1组成的nn方阵,如果按照行来看,每一行就是一个n位的二进制数,这个方阵可以经过旋转变换和镜像变换变成新的方阵,我们假定旋转变换有三种,分别是 「爱情、让人受尽委屈。」/ 2023年02月23日 05:27/ 0 赞/ 41 阅读
相关 螺旋方阵 所谓“螺旋方阵”,是指对任意给定的NNN,将1到N×NN\\times NN×N的数字从左上角第1个格子开始,按顺时针螺旋方向顺序填入N×NN\\times NN×N的方阵里。 骑猪看日落/ 2022年09月27日 06:18/ 0 赞/ 275 阅读
相关 【游戏开发创新】使用Unity制作方阵编队,CSDN方阵迎面走来,感谢CSDN的中秋礼物(图像采样 | 点阵 | 方阵 | 队形 | 变换 | 动画) 文章目录 一、前言 二、运行效果 三、实现原理 四、图片资源 五 刺骨的言语ヽ痛彻心扉/ 2022年09月08日 11:53/ 0 赞/ 273 阅读
相关 转方阵 对一个方阵转置,就是把原来的行号变列号,原来的列号变行号 例如,如下的方阵: 1 2 3 4 5 6 7 8 浅浅的花香味﹌/ 2022年08月08日 13:59/ 0 赞/ 168 阅读
相关 转方阵 对一个方阵转置,就是把原来的行号变列号,原来的列号变行号 例如,如下的方阵: 1 2 3 4 5 6 7 8 9 10 11 待我称王封你为后i/ 2022年08月02日 07:20/ 0 赞/ 159 阅读
相关 螺旋方阵 螺旋方阵 Time Limit: 1000MS Memory Limit: 65536KB [Submit][] [ Statistic][S ゝ一纸荒年。/ 2022年07月03日 14:58/ 0 赞/ 210 阅读
相关 螺旋方阵 Problem Description n×n的螺旋方阵当n=5和n=3时分别是如下的形式 ![1295.png][] 请给出一个程序,对于任意的输入n(0 墨蓝/ 2022年06月17日 05:28/ 0 赞/ 227 阅读
相关 螺旋方阵 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 所谓“螺旋方阵”,是指对任意给定的N,将1到N\N的 左手的ㄟ右手/ 2022年05月29日 05:25/ 0 赞/ 239 阅读
相关 方阵相乘 一 代码 package Matrix; / Copyright (C), 2020-2020, XXX有限公司 FileNam 约定不等于承诺〃/ 2021年07月24日 16:15/ 0 赞/ 336 阅读
还没有评论,来说两句吧...