【Unity】調節自在な3Dルーレットの実装

【Unity】3Dルーレットの各パターンの設置&動作テスト

エディタ拡張を使用してゲーム実行前に設置出来る、カスタマイズ可能な3Dルーレット。
2D版と違い、ベース部分を可変に出来ないので、画像素材や3Dモデルを多めに用意。

2、3、4、5、6、8、10、12ピースに対応。

2D(UI)版はこちら↓

サンプル動画

  • 左側: ワールド空間に2DルーレットVer
  • 右側: 3DモデルVer

各パターンの設置&動作テスト

ルーレット結果の精度テスト

画像&3Dモデル素材

  • ライセンス: CC0

画像の設定

  1. Projectウィンドウの任意のフォルダに、ドラッグ&ドロップしてインポート。
  2. 対象画像を選択。
  3. インスペクター -> Texture TypeをSprite (2D and UI)に変更。
  4. Apply

コード

RouletteManager (ワールド空間に2Dのルーレットを設置Ver)

  1. 要素数を決めて、それに応じたスプライトを紐付けしておく。
  2. (要素数が6以上の場合は)色配列と、文字配列を追加で設定。
  3. 各種設定を任意に調整。
  4. インスペクター最下部のCreateをクリック。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class RouletteManager : MonoBehaviour
{

	[HideInInspector]
	[SerializeField]
	Transform tf;

	[HideInInspector]
	[SerializeField]
	Transform elementsTf;

	[Header("ルーレットの1ピースの画像を紐付け")]
	[SerializeField]
	Sprite elementSprite;

	[Header("ルーレットの針の画像を紐付け")]
	[SerializeField]
	Sprite clapperSprite;

	[Header("ルーレットの要素数")]
	[SerializeField]
	[Range(2, 12)]
	int elementCount = 6;

	[Header("ルーレットの要素の色を指定 (要素数と配列数を同じにしてください)")]
	[SerializeField]
	Color[] elementColors = {
		new Color32(255, 0, 0, 255),
		new Color32(255, 128, 0, 255),
		new Color32(255, 255, 0, 255),
		new Color32(128, 255, 0, 255),
		new Color32(0, 255, 128, 255),
		new Color32(0, 255, 255, 255),
	};

	[Header("ルーレットのサイズ (Scale)")]
	[SerializeField]
	[Range(0.1f, 10.0f)]
	float rouletteSize = 1.0f;

	[Header("ルーレットの針のサイズ (Scale)")]
	[SerializeField]
	[Range(0.03f, 3.0f)]
	float clapperSize = 0.2f;

//(マイナスで逆回転)
	[Header("回転するスピード (秒速)")]
	[SerializeField]
	[Range(-1080, 1080)]
	float spinSpeed = 360;

	[HideInInspector]
	[SerializeField]
	float elementRotationToAdd;

//コルーチン管理用。
	Coroutine spin;

	[Header("結果のインデックス (elementCountが6の場合、0~5)")]
	public int resultElementIndex;

	[Header("(回転終了時の)慣性の長さ (秒)")]
	[SerializeField]
	[Range(0.1f, 10.0f)]
	float inertiaDuration = 0.5f;

	float inertiaElapsedTime;

	[Header("ルーレットを設置するオブジェクトの半分の厚み")]
	[SerializeField]
	[Range(0.1f, 5.0f)]
	float parentHalfDepth = 0.5f;



//要素に入れる文字関係。

//フォントを指定しない場合はデフォルトの物が使われる。
	[Header("文字のフォント")]
	[SerializeField]
	TMP_FontAsset font;

	[Header("要素毎に入れる文字のサイズ (Scale)")]
	[SerializeField]
	[Range(0.01f, 1.0f)]
	float fontSize = 0.1f;

	[Header("文字の色")]
	[SerializeField]
	Color tmpColor = new Color32(255, 255, 255, 255);

	[Header("各要素に表示するテキスト (要素数と配列数を同じにしてください)")]
	[SerializeField]
	string[] tmpTexts = new string[] {
		"0",
		"1",
		"2",
		"3",
		"4",
		"5",
	};


//ルーレット生成時に一時的に使用。
	GameObject elementGo;
	Transform elementTf;

	GameObject elementSrGo;
	Transform elementSrTf;
	SpriteRenderer elementSr;

	GameObject elementTmpGo;
	Transform elementTmpTf;
	TextMeshPro elementTmp;

	Vector2 elementSpriteSize;
	Vector2 clapperSpriteSize;



	void Awake()
	{
		if (tf == null)
			tf = transform;

		if (tf.childCount == 0)
			CreateRoulette();



	}



//ルーレットの回転をスタートしたい時にコレを呼ぶ。
	public void StartSpin()
	{
//一応、多重実行を防止。
		if (spin != null)
			StopCoroutine(spin);

		spin = StartCoroutine(Spin());
	}


	IEnumerator Spin()
	{
		while (true) {

			elementsTf.Rotate(Vector3.forward * spinSpeed * Time.deltaTime);


//画面をタップ(クリック)で、徐々に停止。
			if (Input.GetMouseButtonDown(0)) {
				inertiaElapsedTime = 0;

				while (true) {
					inertiaElapsedTime += Time.deltaTime;

					elementsTf.Rotate(Vector3.forward * Mathf.Lerp(spinSpeed, 0, inertiaElapsedTime / inertiaDuration) * Time.deltaTime);


					if (inertiaDuration <= inertiaElapsedTime) {

						resultElementIndex = Mathf.FloorToInt(elementsTf.eulerAngles.z / elementRotationToAdd % elementCount);

//ここでresultElementIndexに応じた処理を入れる。
//止まった領域の文字はtmpTexts[resultElementIndex]で取得可能。

						spin = null;
						yield break;
					}

					yield return null;
				}
			}

			yield return null;
		}
	}


	public void CreateRoulette()
	{
		if (elementSprite == null || clapperSprite == null) {
			print("インスペクターから各Spriteを紐付けしてください!");
			return;
		}

		if (tf == null)
			tf = transform;


		elementRotationToAdd = 360.0f / elementCount;
		elementSpriteSize = elementSprite.bounds.size;
		clapperSpriteSize = clapperSprite.bounds.size;


		elementGo = new GameObject("RouletteElements");
		elementsTf = elementGo.transform;
		elementsTf.SetParent(tf);
		elementsTf.localPosition = new Vector3(0, 0, -parentHalfDepth - 0.01f);


		for (int i = 0; i < elementCount; i++) {
			elementGo = new GameObject(System.String.Format("RouletteElement{0}", i + 1));
			elementTf = elementGo.transform;
			elementTf.SetParent(elementsTf);
			elementTf.localPosition = Vector3.zero;


			elementSrGo = new GameObject(System.String.Format("Sr{0}", i + 1));
			elementSrTf = elementSrGo.transform;
			elementSrTf.SetParent(elementTf);
			elementSrTf.localScale = new Vector3(rouletteSize / 2 * (1.0f / elementSpriteSize.y), rouletteSize / 2 * (1.0f / elementSpriteSize.y), 1.0f);
			elementSrTf.localPosition = new Vector3(0, rouletteSize / 4, 0);

			elementSr = elementSrGo.AddComponent<SpriteRenderer>();
			elementSr.sprite = elementSprite;

			if (i < elementColors.Length)
				elementSr.color = elementColors[i];


			elementTmpGo = new GameObject(System.String.Format("Tmp{0}", i + 1));
			elementTmpTf = elementTmpGo.transform;
			elementTmpTf.SetParent(elementTf);
			elementTmpTf.localPosition = new Vector3(0, rouletteSize / 4 + fontSize / 2, -0.01f);
			elementTmp = elementTmpGo.AddComponent<TextMeshPro>();

			if (font != null)
				elementTmp.font = font;

//大体フォントサイズ10で、Scale1程度の大きさになった。
			elementTmp.fontSize = fontSize * 10.0f;
			elementTmp.enableWordWrapping = false;
			elementTmp.alignment = TextAlignmentOptions.Center;
//一応、リッチテキストを無効化している(使う場合は戻してください)。
			elementTmp.richText = false;
			elementTmp.sortingOrder = 1;


			if (i < tmpTexts.Length)
				elementTmp.text = tmpTexts[i];
			elementTmp.color = tmpColor;



			elementTf.Rotate(Vector3.back * (elementRotationToAdd / 2 + elementRotationToAdd * i));
		}


		elementGo = new GameObject("Clapper");
		elementTf = elementGo.transform;
		elementTf.SetParent(tf);
		elementTf.localScale = new Vector3(clapperSize * (1.0f / clapperSpriteSize.y), clapperSize * (1.0f / clapperSpriteSize.y), 1.0f);
		elementTf.localPosition = new Vector3(0, rouletteSize / 2, -parentHalfDepth - 0.02f);

		elementSr = elementGo.AddComponent<SpriteRenderer>();
		elementSr.sprite = clapperSprite;
		elementSr.sortingOrder = 2;
	}
}

RouletteManager (3DモデルVer)

  1. 要素数を決めて、それに応じた3Dモデルを紐付けしておく。
  2. (要素数が6以上の場合は)色配列と、文字配列を追加で設定。
  3. 各種設定を任意に調整。
  4. インスペクター最下部のCreateをクリック。


using System.Collections;
using UnityEngine;
using TMPro;

public class RouletteManager : MonoBehaviour
{

	[HideInInspector]
	[SerializeField]
	Transform tf;

	[HideInInspector]
	[SerializeField]
	Transform elementsTf;

	[Header("ルーレットの3Dモデルを紐付け")]
	[SerializeField]
	GameObject rouletteModelGo;

	[Header("ルーレットの針の3Dモデルを紐付け")]
	[SerializeField]
	GameObject clapperModelGo;

	[Header("ルーレットの要素数")]
	[SerializeField]
	[Range(2, 12)]
	int elementCount = 6;

	[Header("ルーレットの要素の色を指定 (要素数と配列数を同じにしてください)")]
	[SerializeField]
	Color[] elementColors = {
		new Color32(255, 0, 0, 255),
		new Color32(255, 128, 0, 255),
		new Color32(255, 255, 0, 255),
		new Color32(128, 255, 0, 255),
		new Color32(0, 255, 128, 255),
		new Color32(0, 255, 255, 255),
	};

	[Header("ルーレットのサイズ (Scale)")]
	[SerializeField]
	[Range(0.1f, 10.0f)]
	float rouletteSize = 1.0f;

	[Header("回転するスピード (秒速)")]
	[SerializeField]
	[Range(-1080, 1080)]
	float spinSpeed = 360;

	[HideInInspector]
	[SerializeField]
	float elementRotationToAdd;

//コルーチン管理用。
	Coroutine spin;

	[Header("結果のインデックス (elementCountが6の場合、0~5)")]
	public int resultElementIndex;

	[Header("(回転終了時の)慣性の長さ (秒)")]
	[SerializeField]
	[Range(0.1f, 10.0f)]
	float inertiaDuration = 0.5f;

	float inertiaElapsedTime;

	[Header("ルーレットを設置するオブジェクトの半分の厚み")]
	[SerializeField]
	[Range(0.1f, 5.0f)]
	float parentHalfDepth = 0.5f;


//要素に入れる文字関係。

//フォントを指定しない場合はデフォルトの物が使われる。
	[Header("文字のフォント")]
	[SerializeField]
	TMP_FontAsset font;

	[Header("要素毎に入れる文字のサイズ (Scale)")]
	[SerializeField]
	[Range(0.01f, 1.0f)]
	float fontSize = 0.1f;

	[Header("文字の色")]
	[SerializeField]
	Color tmpColor = new Color32(255, 255, 255, 255);

	[Header("各要素に表示するテキスト (要素数と配列数を同じにしてください)")]
	[SerializeField]
	string[] tmpTexts = new string[] {
		"0",
		"1",
		"2",
		"3",
		"4",
		"5",
	};

//ルーレットの向き(インスペクターからは指定し辛いので、コードで指定)。
	Quaternion rouletteRotation = Quaternion.Euler(-90, 0, 0);

//ルーレット生成時に一時的に使用。
	GameObject elementGo;
	Transform elementTf;

	GameObject elementTmpGo;
	Transform elementTmpTf;
	TextMeshPro elementTmp;

	MeshRenderer[] elementMrs;

	Material[] elementMaterials;



	void Awake()
	{
		if (tf == null)
			tf = transform;

		if (tf.childCount == 0)
			CreateRoulette();


	}



//ルーレットの回転をスタートしたい時にコレを呼ぶ。
	public void StartSpin()
	{
//一応、多重実行を防止。
		if (spin != null)
			StopCoroutine(spin);

		spin = StartCoroutine(Spin());
	}


	IEnumerator Spin()
	{
		while (true) {

			elementsTf.Rotate(Vector3.forward * spinSpeed * Time.deltaTime);


//画面をタップ(クリック)で、徐々に停止。
			if (Input.GetMouseButtonDown(0)) {
				inertiaElapsedTime = 0;

				while (true) {
					inertiaElapsedTime += Time.deltaTime;

					elementsTf.Rotate(Vector3.forward * Mathf.Lerp(spinSpeed, 0, inertiaElapsedTime / inertiaDuration) * Time.deltaTime);


					if (inertiaDuration <= inertiaElapsedTime) {

						resultElementIndex = Mathf.FloorToInt(elementsTf.eulerAngles.z / elementRotationToAdd % elementCount);

//ここでresultElementIndexに応じた処理を入れる。
//止まった領域の文字はtmpTexts[resultElementIndex]で取得可能。

						spin = null;
						yield break;
					}

					yield return null;
				}
			}

			yield return null;
		}
	}


	public void CreateRoulette()
	{
		if (rouletteModelGo == null || clapperModelGo == null) {
			print("インスペクターから各3Dモデルを紐付けしてください!");
			return;
		}

		if (tf == null)
			tf = transform;

		elementRotationToAdd = 360.0f / elementCount;


		elementGo = new GameObject("RouletteElements");
		elementsTf = elementGo.transform;
		elementsTf.SetParent(tf);
		elementsTf.localPosition = new Vector3(0, 0, -parentHalfDepth);


		elementGo = Instantiate(rouletteModelGo, elementsTf);
		elementTf = elementGo.transform;
		elementTf.rotation = rouletteRotation;
		elementTf.localScale = Vector3.one * rouletteSize;
		elementTf.localPosition = Vector3.zero;

		elementMrs = elementsTf.GetComponentsInChildren<MeshRenderer>();
		elementMaterials = new Material[elementCount];


		for (int i = 0; i < elementCount; i++) {

			if (i < elementColors.Length) {
				elementMaterials[i] = new Material(Shader.Find("Standard"));
				elementMaterials[i].color = elementColors[i];
				elementMrs[i].material = elementMaterials[i];
			}


			elementTmpGo = new GameObject(System.String.Format("Tmp{0}", i + 1));
			elementTmpTf = elementTmpGo.transform;
			elementTmpTf.SetParent(elementsTf);
			elementTmpTf.localPosition = new Vector3(0, rouletteSize / 4 + fontSize / 2, -0.06f * rouletteSize);

//平面Verと違って、文字をピースとまとめて回転出来ないので、文字のみをRotateAroundでズラす。
			elementTmpTf.RotateAround(elementsTf.position, Vector3.back, elementRotationToAdd / 2 + elementRotationToAdd * i);

			elementTmp = elementTmpGo.AddComponent<TextMeshPro>();

			if (font != null)
				elementTmp.font = font;

//大体フォントサイズ10で、Scale1程度の大きさになった。
			elementTmp.fontSize = fontSize * 10.0f;
			elementTmp.enableWordWrapping = false;
			elementTmp.alignment = TextAlignmentOptions.Center;
//一応、リッチテキストを無効化している(使う場合は戻してください)。
			elementTmp.richText = false;
			elementTmp.sortingOrder = 1;


			if (i < tmpTexts.Length)
				elementTmp.text = tmpTexts[i];
			elementTmp.color = tmpColor;

		}


		elementGo = Instantiate(clapperModelGo, tf);
		elementTf = elementGo.transform;
		elementTf.rotation = rouletteRotation;
		elementTf.localScale = Vector3.one * rouletteSize;
		elementTf.localPosition = new Vector3(0, rouletteSize / 2, -parentHalfDepth);
	}
}

RouletteManagerEditor (インスペクターに生成ボタンを出す為のエディタ拡張)

  • 両パターン共通で必須。
  • Editorフォルダ下に入れておく。
    (無い場合は新規作成)


using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(RouletteManager))]
public class RouletteManagerEditor : Editor
{
	RouletteManager rouletteManager;

	public override void OnInspectorGUI()
	{
		base.OnInspectorGUI();

		if (rouletteManager == null)
			rouletteManager = target as RouletteManager;


		if (GUILayout.Button("Create")){
			rouletteManager.CreateRoulette();
		}
	}
}

使用手順

  • (2D版、2.5D版、3D版で共通)

  1. GameManager等で、RouletteManagerへの参照を保持しておく。
  2. 回転をスタートさせたい時に、RouletteManagerのStartSpinを呼ぶ。
  3. 画面タップで徐々に回転が停止。
    (Spinコルーチンの終了部分へ、結果に応じた処理を追加しておく)

影が汚い場合

影をなくすパターン

  1. ルーレットの3Dモデルを選択。
  2. Mesh Rendererコンポーネント -> shadowCastingModeをOffに設定。

グラフィックの設定を変更するパターン

  • Project Settings -> Quality -> Shadow Distanceを低めに調整。

タイトルとURLをコピーしました