前置き
ブロックでステージが構成されるゲームでは、エディタ上で毎回ステージを配置するよりも、ゲーム内で直接編集できる制作機能を用意することで、試作や調整を効率化できます。
本記事では、その中でもマウス位置にブロックを設置する機能の実装方法を解説します。
手法
3Dゲームにおいてブロックを設置する座標を取得するには大きく分けて2つの手法があります。
-
Physics.Raycastがすでに存在するブロックの側面に衝突した際の座標を参照する方法 - マウス位置から生成したRayと、任意平面(今回はY座標面)との交点を取得する方法
前者にレイキャストの範囲を付け加えたものがMinecraftで使用されている手法だと思われます。
今回はステージ作成機能ということもあり、シーン上に何もない場合においても、ブロックの設置が可能な後者の実装方法について解説します。
解説
1. 衝突座標面の調整
今回は衝突座標面をY座標面として、Y座標面の高さの調節方法について記述します。
以下はマウスのスクロールによってY座標を増減するコードです。
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.InputSystem;
public class BlockManipulator : MonoBehaviour
{
private float m_targetY = 0f; // 座標面の高さ
private const float scrollThreshold = 0.5f; // Y座標の繰り上げ用
private float scrollAccumulator = 0f; // スクロールの和の値
void Update()
{
if(Input.GetAxis("Mouse ScrollWheel") != 0f)
{
HandleMouseWheelInput();
}
}
/// <summary>
/// マウスホイールの入力を処理し、Y座標のターゲット値を更新する
/// </summary>
private void HandleMouseWheelInput()
{
float scrollY = Input.GetAxis("Mouse ScrollWheel");
scrollAccumulator += scrollY;
if (scrollAccumulator >= scrollThreshold)
{
m_targetY++;
scrollAccumulator -= scrollThreshold;
}
else if (scrollAccumulator <= -scrollThreshold)
{
m_targetY--;
scrollAccumulator += scrollThreshold;
}
}
}
2. マウス位置から設置座標を取得する
ブロック設置位置は、マウスカーソル位置からカメラ方向へ伸ばしたレイと、現在選択中の Y座標平面 との交点から求めます。
Unityの Physics.Raycast() はColliderが必要ですが、本手法では空間上の仮想平面に対して計算するため、シーン上に何も無い場所でも設置位置を取得できます。
交点は次の式で求められます。
t = (targetY - ray.origin.y) / ray.direction.y
このtを用いて、
hitPoint = ray.origin + ray.direction * t
とすることで、平面上の座標を取得できます。
また、1×1×1のブロックで構成されるステージにおいては、座標を整数値に丸め込む必要があります。以下の様に記述することで丸め込むことができます。
Vector3 snapped = new Vector3(Mathf.Round(hitPoint.x), m_targetY, Mathf.Round(hitPoint.z));
ここまでの処理をまとめると、以下のようになります。
private Vector2 m_lastMousePosition; // 更新前のx,z座標値
private Vector3 m_lastSnapped; // 更新前の丸め込み座標
private Camera m_mainCamera;
void Awake()
{
m_mainCamera = Camera.main;
}
/// <summary>
/// 丸め込んだ座標の取得
/// </summary>
/// <returns></returns>
private Vector3 GetSnappedPoint()
{
Vector2 currentMousePosition = Input.mousePosition;
Ray ray = m_mainCamera.ScreenPointToRay(currentMousePosition);
// マウスが動いていなければX,Zは前回値を使いYだけ更新
if (currentMousePosition == m_lastMousePosition)
{
m_lastSnapped.y = m_targetY;
return m_lastSnapped;
}
//レイと平面の交点から新しい座標を計算
float t = (m_targetY - ray.origin.y) / ray.direction.y;
if (Mathf.Abs(ray.direction.y) < 0.0001f || t < 0f)
{
return Vector3.zero;
}
Vector3 hitPoint = ray.origin + ray.direction * t;
Vector3 snapped = new Vector3(Mathf.Round(hitPoint.x), m_targetY, Mathf.Round(hitPoint.z));
// 前回値を更新
m_lastMousePosition = currentMousePosition;
m_lastSnapped = snapped;
return snapped;
}
3. プレビューブロックの生成
ブロックを設置する際に、プレビュー表示を行うことで、設置ミスを減らしステージの操作性を向上させることができます。
先ほどのコードから座標を取得し、ブロックを生成します。
m_previewBlock = GameObject.CreatePrimitive(PrimitiveType.Cube);
m_previewBlock.transform.position = nowMousePosition;
CreatePrimitive では Plane Cube sphere cylinder capsule quad といったUnity標準のプリミティブモデルを生成することができます。
private GameObject m_previewBlock; // プレビューブロックを保持する変数
private Vector3 m_prePosition; // 直前の交差座標を保持する変数
/// <summary>
/// プレビューブロックの生成
/// </summary>
private void PreviewBlock()
{
Vector3 nowMousePosition = GetSnappedPoint();
if(m_previewBlock == null)
{
m_previewBlock = GameObject.CreatePrimitive(PrimitiveType.Cube);
Destroy(m_previewBlock.GetComponent<Collider>());
}
if(m_prePosition != nowMousePosition)
{
m_previewBlock.transform.position = nowMousePosition;
m_prePosition = nowMousePosition;
}
}
もし、プレビューブロックに自前で用意したオブジェクトを用いるのであれば CreatePrimitive ではなく、 Instantiate を用いることをお勧めします。
4. ブロックの座標管理
ブロックの設置・削除を行う際に、指定した座標にブロックが存在するかの判定を行う必要があります。今回は Dictionary を用いて、座標とブロック情報を管理します。これにより高速に存在判定を行えるほか、ステージデータをJSONとしてエクスポートする際にも同じ情報を流用できます。
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// ステージのブロックID・座標の管理スクリプト
/// </summary>
public class StageBlockManager : MonoBehaviour
{
Dictionary<Vector3Int, int> m_blockTypeMap = new(); // 座標とブロックの種類を記録(ステージデータエクスポート用)
Dictionary<Vector3Int, GameObject> m_blockObjectMap = new();
/// <summary>
/// ブロックのID, 座標情報を登録
/// </summary>
public void RegisterBlock(Vector3Int blockPosition, int blockID, GameObject cubeObject)
{
m_blockTypeMap[blockPosition] = blockID;
m_blockObjectMap[blockPosition] = cubeObject;
}
/// <summary>
/// ブロック座標を元に削除
/// </summary>
/// <param name="blockPosition"></param>
public void RemoveBlock(Vector3Int blockPosition)
{
if (m_blockObjectMap.TryGetValue(blockPosition, out var obj))
{
Destroy(obj);
}
m_blockTypeMap.Remove(blockPosition);
m_blockObjectMap.Remove(blockPosition);
}
/// <summary>
/// 既に同じ座標にブロックが存在するかどうかの判定
/// </summary>
/// <param name="blockPosition"></param>
/// <returns></returns>
public bool IsBlockOccupied(Vector3 blockPosition)
{
Vector3Int blockIntPosition = new Vector3Int((int)blockPosition.x, (int)blockPosition.y, (int)blockPosition.z);
return m_blockTypeMap.ContainsKey(blockIntPosition);
}
}
ブロックIDとGameObjectを分けて保持することで、表示オブジェクトが不要な場面でもステージデータのみを扱いやすくしています。
5. ブロックの生成・削除
プレビューを表示した位置に、実際にブロックを生成します。
この際に、再度座標計算を行うのではなく、プレビュー更新時に保存している m_prePosition を利用します。これにより、処理を簡潔にしつつプレビュー位置と実際の生成位置がズレるのを防ぐことができます。
[SerializeField]
Transform m_stageParentObject;
[SerializeField]
StageBlockManager m_stageBlockManager;
/// <summary>
/// ブロックのセット
/// </summary>
private void SetBlock()
{
if(!m_stageBlockManager.IsBlockOccupied(m_prePosition))
{
GameObject blockPrefab = m_blockHotbar.GetSelectedBlockData() != null?m_blockHotbar.GetSelectedBlockData().prefab : null;
if (blockPrefab != null)
{
GameObject instance = Instantiate(blockPrefab, m_prePosition, Quaternion.identity, m_stageParentObject);
m_stageBlockManager.RegisterBlock(new Vector3Int((int)m_prePosition.x, (int)m_prePosition.y, (int)m_prePosition.z), 0, instance);
// 生成したプレハブの名前を変更
instance.name = blockPrefab.name + "_Instance";
}
}
}
次に設置済みのブロックを削除する処理を記述します。
/// <summary>
/// ブロックの削除
/// </summary>
private void DeleteBlock()
{
if(m_stageBlockManager.IsBlockOccupied(m_prePosition))
{
m_stageBlockManager.RemoveBlock(new Vector3Int((int)m_prePosition.x, (int)m_prePosition.y, (int)m_prePosition.z));
}
}
結果
結論
本記事では、ゲーム内でブロックを設置する制作機能として、
- Y座標面の切り替え
- マウス位置からの設置座標取得
- プレビュー表示
- Dictionaryによる座標管理
- ブロックの生成・削除
までを実装しました。
エディタ上で毎回配置するよりも、ゲーム内で直接編集できることで試作や調整の速度を大きく向上させることができます。
今後はホットバーで指定したオブジェクトの生成や範囲選択配置・保存機能・Undo/Redo などを追加することで、より実用的なステージエディタへ発展させていく予定です。
