文章目錄
- 一、前言
- 二、運行效果
- 三、實作原理
- 四、圖片資源
- 五、模型資源
- 六、像素采樣,生成點陣
- 七、根據點陣生成角色方陣
- 八、方陣行走
- 九、方陣變換
- 十、工程原始碼
- 十一、完畢
一、前言
嗨,大家好,我是新發,
最近一直在忙一些事情,好幾天沒寫文章了,前天收到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>();
生成點陣的邏輯如下:
// 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);
}
}
}
效果:

可以調節采樣梯度和坐標縮放,

如下:

七、根據點陣生成角色方陣
我們創建一個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 移動、旋轉、播影片
}
}
此時的效果:

八、方陣行走
我們要讓方陣跑起來,每個角色朝著自己的位置移動、旋轉,并且配套播放跑步和站立的影片,
這里需要要讓狀態過渡比較自然,我是根據距離來決定影片混合,使用線性差值來計算旋轉,代碼如下:
代碼如下:
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面板中設定資料啦~

寫個方法實作方陣變換,
// 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();
}
}
效果如下:

十、工程原始碼
本工程我已上傳到CODE CHINA,感興趣的同學可自行下載學習,
地址:https://codechina.csdn.net/linxinfa/UnityFormationsDemo
注:我使用的Unity版本為Unity 2021.1.9f1c1 (64-bit),

十一、完畢
好了,就到這里吧,
我是林新發:https://blog.csdn.net/linxinfa

原創不易,若轉載請注明出處,感謝大家~
喜歡我的可以點贊、關注、收藏,如果有什么技術上的疑問,歡迎留言或私信,我們下期見~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296788.html
標籤:其他
