前作のキューブの破壊は?
昔はこんな感じでキューブの位置から、キューブのデータを特定していた模様
internal unsafe bool GetCubeByPosition(Vector3 position, out Vector3Int chunkIndex, out Vector3Int cubeIndex, out CubeSt cube) { chunkIndex = new Vector3Int( Mathf.FloorToInt(position.x / (ChunkSizeX * CubeSide)), Mathf.FloorToInt(position.y / (ChunkSizeY * CubeSide)), Mathf.FloorToInt(position.z / (ChunkSizeZ * CubeSide))); var origin = _chunkTensor.GetFromOrigin(Vector3Int.zero); if (null != origin.value) { var originChunkIndex = origin.value.chunkIndex; var chunkObject = _chunkTensor.GetFromOrigin(new Vector3Int(chunkIndex.x - originChunkIndex.x, chunkIndex.y - originChunkIndex.y, chunkIndex.z - originChunkIndex.z)).value; if (null != chunkObject) { var entity = chunkObject.gameObject.GetComponent<GameObjectEntity>().Entity; var em = World.Active.GetExistingManager<EntityManager>(); ChunkData data = em.GetComponentData<ChunkData>(entity); int x = (int)(position.x / CubeSide) % ChunkSizeX; x = 0 > x ? ChunkSizeX + x : x; int y = (int)(position.y / CubeSide) % ChunkSizeY; y = 0 > y ? ChunkSizeY + y : y; int z = (int)(position.z / CubeSide) % ChunkSizeZ; z = 0 > z ? ChunkSizeZ + z : z; cube = new CubeSt(); cubeIndex = new Vector3Int(x, y, z); int ct = (x * ChunkSizeZ * ChunkSizeY + z * ChunkSizeY + y) * CubeSt.size; cube.cat = (CubeCategory)data.data[ct + 0]; cube.rot = (CubeRotation)data.data[ct + 1]; cube.sideA = (SideType)data.data[ct + 2]; cube.sideB = (SideType)data.data[ct + 3]; return true; } } cube = new CubeSt(); cubeIndex = Vector3Int.zero; return false; }
chunkTensor? 知らない子ですね(ひどい!頑張ったのに無かったことにされてしまった)
空間座標からチャンクのインデックスまで求めるのは良さそうですね。
あとはチャンク内でのインデックスがわかればよし
前回の UI 操作で、マウスの左クリックイベントを発行して、ハンドラで座標を割り出しましょう。
ここでひらめくのは、対象オブジェクトがわかるわけで、それはチャンクなわけで…
あ!先にもこもこやります。
もこもことは?
カーソルが触れたキューブがゆっくりと大きくなり、そしてカーソルが外れると、ゆっくりと小さくなるキューブが
必要な時に現れて、不要な時に消えることを指します。
カーソルはこんな感じで、辞書から位置を使ってアクセスできるようにし
Dictionary<Vector3Int, GameObject> mocomocoCubes = new Dictionary<Vector3Int, GameObject>();
カーソルの先に触れたキューブとして、なければ Instantiate, あればそれを表示
こ、これだと無限に居続けるので、3秒したら辞書から自身を消して Destroy するようにしてみます。(+現れたときに大きくなり始め、消えるまで小さくなり続ける)
想像していたのと違う…
カーソルが当たっている間は大きいままでいてほしく、カーソルが外れたら小さくなりはじめてほしい
こんな感じですね。
キューブの Prefab に割り当てたコンポーネントがこちら(毎回思うけど、4次元アニメーションを1次元コードに落とし込むこの作業はな…集中力をごっそりもってかれる)
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 大きくなって、元のサイズに戻った時に消える /// </summary> public class SelectedCubeAnimator : MonoBehaviour { #region Params [SerializeField] float selectedScale = 1.5f; [SerializeField] float scaleSpeed = 0.01f; #endregion public void ScaleStart() { this.mocomocoCubes = null; if (null != this.coroutine) { StopCoroutine(this.coroutine); } coroutine = StartCoroutine(this.CoScaleThis()); } public void ScaleEnd(Dictionary<Vector3Int, GameObject> mocomocoCubes) { this.mocomocoCubes = mocomocoCubes; if (null != this.coroutine) { StopCoroutine(this.coroutine); } coroutine = StartCoroutine(this.CoDestroyThis()); } IEnumerator CoScaleThis() { while (this.selectedScale > this.transform.localScale.x) { this.transform.localScale = Vector3.one * (this.transform.localScale.x + this.scaleSpeed); yield return null; } } IEnumerator CoDestroyThis() { while (1 < this.transform.localScale.x) { this.transform.localScale = Vector3.one * (this.transform.localScale.x - this.scaleSpeed); yield return null; } if (null != this.mocomocoCubes) { this.mocomocoCubes.Remove(new Vector3Int((int)this.transform.position.x, (int)this.transform.position.y, (int)this.transform.position.z)); Destroy(this.gameObject); } } Coroutine coroutine; Dictionary<Vector3Int, GameObject> mocomocoCubes; }
キューブデータの特定
触れている Chunk オブジェクトは RayCast の戻り値で得られているはずなので、そこからチャンクのインデックスを求めてみます。
いい感じですね。
実装は次の通り変化しました。(MeshSystemの修正を頑張った…Mesh API v2 使ってるんですよ、新しいです)
using CubeWalk; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; using UnityEngine.Rendering; /// <summary> /// カーソルが当たったキューブの位置にアニメーションするキューブを配置 /// </summary> public class CubeSelector : MonoBehaviour { #region SceneComponents [SerializeField] InputModeSwitcher inputModeSwitcher; [SerializeField] GameObject cursorCube; [SerializeField] CubeDataWorld cubeDataWorld; [SerializeField] ChunkMeshBehaviour chunkMeshBehaviour; #endregion void Start() { this.inputModeSwitcher.onChangeInputMode += OnChangeInputMode; } private void OnChangeInputMode(bool showCursor) { // カーソルが表示されているフラグを立てる this.existingCursor = showCursor; if (!showCursor && this.mocomocoCubes.TryGetValue(this.currentCubePosition, out GameObject cube)) { // 以前のキューブを削除 var animator = cube.GetComponent<SelectedCubeAnimator>(); animator.ScaleEnd(this.mocomocoCubes); this.cubeSelected = false; } } void Update() { if (!this.existingCursor) return; // スクリーン中のマウスポインタに向かってカメラからレイを作成 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); LocateCursorCube(ray); } private void LocateCursorCube(Ray ray) { // レイが CubeChunk にヒットした場合にキューブの位置を特定 if (this.cubeSelected = Physics.Raycast(ray, out RaycastHit cameraHit, IntaractRayLength, LayerMask.GetMask(LayerMask_Level))) { Vector3 cubePosition = cameraHit.point - (cameraHit.normal * ChunkConst.CubeSide * 0.5f); Vector3 roundSource = cubePosition / ChunkConst.CubeSide; Vector3Int gridCubePosition = new Vector3Int(Mathf.RoundToInt(roundSource.x), Mathf.RoundToInt(roundSource.y), Mathf.RoundToInt(roundSource.z)); Vector3 newCubePositon = new Vector3(gridCubePosition.x, gridCubePosition.y, gridCubePosition.z) * ChunkConst.CubeSide; // 現在配置しているカーソルキューブの位置からずれたか確認 if (0 == this.mocomocoCubes.Count || this.currentCubePosition != gridCubePosition) { if (this.mocomocoCubes.TryGetValue(this.currentCubePosition, out GameObject cube)) { // 前のキューブを削除 var animator = cube.GetComponent<SelectedCubeAnimator>(); animator.ScaleEnd(this.mocomocoCubes); } // カーソルキューブを再配置 if (!this.mocomocoCubes.TryGetValue(gridCubePosition, out cube)) { // 辞書になければ追加 cube = Instantiate(this.cursorCube, this.transform); #region マテリアルの設定 if (!chunkMeshBehaviour.materials.ContainsKey(chunkMeshBehaviour.meshShader.GetInstanceID())) { var material = new Material(chunkMeshBehaviour.meshShader); material.SetTexture("Texture2D_5418CE01", chunkMeshBehaviour.textureAlbedo); chunkMeshBehaviour.materials.Add(chunkMeshBehaviour.meshShader.GetInstanceID(), material); } var renderer = cube.GetComponent<MeshRenderer>(); cube.GetComponent<MeshRenderer>().sharedMaterial = chunkMeshBehaviour.materials[chunkMeshBehaviour.meshShader.GetInstanceID()]; #endregion // キューブデータの取得 this.cubeDataWorld.ChunkInt3ToChunkKey(cameraHit.collider.gameObject.GetComponent<ChunkMeshInfo>().chunkInt3, out Vector3Int chunkKeyXYZ); unsafe { var pData = this.cubeDataWorld.CubeDataFromPosition(newCubePositon, chunkKeyXYZ); var rotationType = (CubeRotationType)pData[(int)ChunkDataType.Rotation]; int row = (byte)rotationType % 17; // @debug int col = (byte)rotationType / 17; var vertexCount = 48; // cube verts var nativeVertices = new NativeArray<ChunkMeshSystem.CustomVertexLayout>(vertexCount, Allocator.Temp); var meshData = new ChunkMeshData { vertexCount = vertexCount, pVertices = (float*)nativeVertices.GetUnsafePtr() }; ChunkMeshSystem.FillCubeVertices48(pData, ref meshData); // メッシュ作成 var mesh = new Mesh(); mesh.Clear(); // メッシュレイアウトを定義 var layout = new[] { new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3), new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), }; mesh.SetVertexBufferParams(vertexCount, layout); mesh.SetIndexBufferParams(vertexCount, IndexFormat.UInt32); // インデックスを作成 var nativeIndices = new NativeArray<int>(vertexCount, Allocator.Temp); for (int index = 0; index < vertexCount; index++) { nativeIndices[index] = index; } // 頂点データとインデックスを設定 mesh.SetVertexBufferData(nativeVertices, 0, 0, vertexCount); mesh.SetIndexBufferData(nativeIndices, 0, 0, vertexCount); // ネイティブ配列を開放 nativeVertices.Dispose(); nativeIndices.Dispose(); // サブメッシュの定義 mesh.subMeshCount = 1; mesh.SetSubMesh(0, new SubMeshDescriptor { topology = MeshTopology.Triangles, vertexCount = vertexCount, indexCount = vertexCount, baseVertex = 0, firstVertex = 0, indexStart = 0 }); // 法線と接戦とバウンディングボックスを作成 mesh.RecalculateNormals(); mesh.RecalculateTangents(); mesh.RecalculateBounds(); // メッシュの設定 var meshFilter = cube.GetComponent<MeshFilter>(); if (null != meshFilter.sharedMesh) { meshFilter.sharedMesh.Clear(); } meshFilter.sharedMesh = mesh; } this.mocomocoCubes.Add(gridCubePosition, cube); var animator = cube.GetComponent<SelectedCubeAnimator>(); animator.ScaleStart(); } else { // 既存のキューブを復活 var animator = cube.GetComponent<SelectedCubeAnimator>(); animator.ScaleStart(); } cube.transform.position = newCubePositon; this.currentCubePosition = gridCubePosition; } } else if (this.mocomocoCubes.TryGetValue(this.currentCubePosition, out GameObject cube)) { // 以前のキューブを削除 var animator = cube.GetComponent<SelectedCubeAnimator>(); animator.ScaleEnd(this.mocomocoCubes); } } Dictionary<Vector3Int, GameObject> mocomocoCubes = new Dictionary<Vector3Int, GameObject>(); bool cubeSelected = false; Vector3Int currentCubePosition = Vector3Int.zero; bool existingCursor = false; readonly float IntaractRayLength = 20; readonly string LayerMask_Level = "CubeChunk"; }
マウスの左クリックイベントを発行
誰が?で悩みますが、CubeSelector ですかね。
誰に?はつくらないといけないですね。
キューブの破壊時の様子をイメージしていますが、クリックしたらいきなり破壊はなくて…
キューブの位置、情報、もろもろを受け取りつつ
クリック開始からクリック終了までのステータスを検知し続けて、
破壊については、破壊できない条件なども考えると、プレイヤーがカーソルに込めた道具の要素も一緒に受け取りたいですね。
そんなキューブ破壊を担当するのは Cube Destroyer さん
同じく、キューブの再配置は、左クリック開始と終了ステータスを受け取り、配置前に回転を制御できたりと
そんなキューブを再配置してくれるのが Cube Locator さん
CubeSelector が各種イベントをパラメータ付きで発行して、Destroyer と Locator がこれを購読する枠組みだけ先に用意しちゃいます。
破壊前は、VFX Graph でそのキューブの小さい欠片が舞って、火花とか散って、最後に爆発してほしいなと。。。未来の自分さんに注文つけとこう
Cube Destroyer さん
マウスクリックステートと、キューブを選択していること、キューブのメッシュオブジェクトと、カーソルが触れている位置を取得できています。
VFXEditor で、キューブがわーっとある一点から吹き出してくるもの作れませんか?
そして、一通り Visual Effect Editor の構文を学んだのですが、キューブの世界との干渉は行えず(GPU処理のため)
とくにパフォーマンスは求めなかったので Shuriken を使うことにしました。
そうして注文を受けて、形作られた CubeDestroyer 実装がこちら
using CubeWalk; using UnityEngine; /// <summary> /// キューブの破壊処理 /// </summary> public class CubeDestroyer : MonoBehaviour { #region Scene Components [SerializeField] CubeDataWorld cubeDataWorld; [SerializeField] CubeSelector cubeSelector; [SerializeField] VRMPlayerModelLoader vrmCharacters; #endregion #region Assets [SerializeField] GameObject miningEffectPrefab; [SerializeField] GameObject explosionEffectPrefab; #endregion void Start() { // キューブ選択処理のイベントを購読 this.cubeSelector.onDestroyStarted += this.OnDestroyStarted; this.cubeSelector.onDestroyUpdated += this.OnDestroyUpdated; this.cubeSelector.onDestroyCanceled += this.OnDestroyCanceled; this.cubeSelector.onChangeSelected += this.OnChangeSelected; } /// <summary> /// 破壊開始 /// </summary> void OnDestroyStarted(RaycastHit raycastHit, GameObject cubeObject) { // カリカリというか、そういう掘削時に発生するエフェクトが出現(寿命が来たら自動で消えるようにスケジュール) this.miningEffect = Instantiate(this.miningEffectPrefab, raycastHit.point, Quaternion.LookRotation(raycastHit.normal), this.transform); var particleSystem = this.miningEffect.GetComponent<ParticleSystem>(); Destroy(this.miningEffect, particleSystem.main.duration + particleSystem.main.startLifetime.constant); // エフェクトが利用するレンダラーに対象のキューブメッシュ情報を渡す var renderer = particleSystem.GetComponent<ParticleSystemRenderer>(); renderer.mesh = cubeObject.GetComponent<MeshFilter>().sharedMesh; renderer.sharedMaterials = cubeObject.GetComponent<MeshRenderer>().sharedMaterials; particleSystem.Play(); // キューブ選択スケールを解除 var cubeAnimator = cubeObject.GetComponent<SelectedCubeAnimator>(); cubeAnimator.ScaleEnd(null); // 破壊中 or 破壊時に必要なステート情報を保持 this.chunkCollider = raycastHit.collider; this.cubeObject = cubeObject; this.cubeCenter = cubeObject.transform.position; this.cubeLife = 1.0f; } /// <summary> /// 破壊停止 /// </summary> void OnDestroyCanceled() { CancelSchedule(); // キューブ選択スケールを再開 if (null != this.cubeObject) { var cubeAnimator = this.cubeObject.GetComponent<SelectedCubeAnimator>(); cubeAnimator.ScaleStart(); } } private void CancelSchedule() { // 掘削エフェクト中だった場合は止める if (null != this.miningEffect) { var particleSystem = this.miningEffect.GetComponent<ParticleSystem>(); particleSystem.Stop(); } this.cubeLife = 1.0f; } /// <summary> /// 対象キューブが変わった /// </summary> private void OnChangeSelected() { // 破壊停止と同様にスケジュールを中止 this.CancelSchedule(); } /// <summary> /// 掘削中 /// </summary> void OnDestroyUpdated(RaycastHit raycastHit, GameObject cubeObject) { // 掘削エフェクトがあれば位置と向きを調整 if (null != this.miningEffect) { this.miningEffect.transform.position = raycastHit.point; this.miningEffect.transform.rotation = Quaternion.LookRotation(raycastHit.normal); } this.cubeLife -= 0.01f; // @debug 掘削時間はツールとキューブによって変化させる予定 if (0 > this.cubeLife) { this.cubeLife = 100.0f; // 連続で破壊が行われないようにフタをする // 破壊 this.ExplodeCube(); } } /// <summary> /// 破壊 /// </summary> void ExplodeCube() { // カーソルキューブを一瞬で非表示 var cursorRenderer = this.cubeObject.GetComponent<Renderer>(); cursorRenderer.enabled = false; // 破壊エフェクトを開始&破棄をスケジュール var explosionEffect = Instantiate(this.explosionEffectPrefab, this.cubeCenter, this.miningEffect.transform.rotation, this.transform); var explosionParticleSystem = explosionEffect.GetComponent<ParticleSystem>(); Destroy(explosionEffect, explosionParticleSystem.main.duration); var miningParticleSystem = this.miningEffect.GetComponent<ParticleSystem>(); var renderer = explosionParticleSystem.GetComponent<ParticleSystemRenderer>(); var miningRenderer = miningParticleSystem.GetComponent<ParticleSystemRenderer>(); renderer.mesh = miningRenderer.mesh; renderer.sharedMaterials = miningRenderer.sharedMaterials; explosionParticleSystem.Play(); // バキューム対象をセット var vInput = this.vrmCharacters.GetComponentInChildren<Invector.vCharacterController.vThirdPersonInput>(); if (null != vInput) { var animator = explosionEffect.GetComponent<ExploadedCubeAnimator>(); animator.SetVacuumeOrigin(vInput.transform); } // 対象キューブを空気に var chunkInt3 = this.chunkCollider.gameObject.GetComponent<ChunkMeshInfo>().chunkInt3; this.cubeDataWorld.SetCubeData(this.cubeCenter, chunkInt3, CubeCategoryType.Basic, CubeRotationType.Top000, SideType.Air, SideType.Air); } float cubeLife = 1.0f; GameObject cubeObject = null; GameObject miningEffect = null; Vector3 cubeCenter = Vector3.zero; Collider chunkCollider = null; }
破壊するキューブが隣接するチャンクの特定
キューブを破壊する Mesh 更新処理にも一工夫必要で…
チャンクに隣接している場合はそのチャンクのメッシュも更新しないと世界に穴が空いてしまいます。
そこは過去の経験でこのようにカバー(汚いが、3次元を展開して1次元に書き表すとはこのこと)
void OnUpdateChunkData(Vector3Int chunkInt3, Vector3Int cubeInt3, uint loadTaskGeneratoin) { // もし cube が上のチャンクと接していたら、上のチャンクも更新対象とする if ((ChunkConst.ChunkSizeY - 1) == cubeInt3.y) { var nextChunkInt3 = chunkInt3 + Vector3Int.up; if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo); } } // もし cube が下のチャンクと接していたら、上のチャンクも更新対象とする if (0 == cubeInt3.y) { var nextChunkInt3 = chunkInt3 + Vector3Int.down; if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo); } } // もし cube が右のチャンクと接していたら、右のチャンクも更新対象とする if ((ChunkConst.ChunkSizeX - 1) == cubeInt3.x) { var nextChunkInt3 = chunkInt3 + Vector3Int.right; if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo); } } // もし cube が左のチャンクと接していたら、左のチャンクも更新対象とする if (0 == cubeInt3.x) { var nextChunkInt3 = chunkInt3 + Vector3Int.left; if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo); } } // もし cube が奥のチャンクと接していたら、奥のチャンクも更新対象とする if ((ChunkConst.ChunkSizeZ - 1) == cubeInt3.z) { var nextChunkInt3 = chunkInt3 + new Vector3Int(0, 0, 1); if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo); } } // もし cube が左のチャンクと接していたら、左のチャンクも更新対象とする if (0 == cubeInt3.z) { var nextChunkInt3 = chunkInt3 + new Vector3Int(0, 0, -1); if (!this.immediateUpdateEntityMap.ContainsKey(nextChunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = nextChunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(nextChunkInt3, createChunkInfo); } } // 対象チャンク if (!this.immediateUpdateEntityMap.ContainsKey(chunkInt3)) { var createChunkInfo = new CreateChunkInfo { chunkInt3 = chunkInt3, combineCoreChunkIndex = -1, loadTaskGeneration = loadTaskGeneratoin }; this.immediateUpdateEntityMap.Add(chunkInt3, createChunkInfo); } }
キューブの破壊の適用結果
上記の実装を経て、やっと心象風景の具現化ができてきました。
こちらが、「キューブの破壊」のアウトプットです。