【Unity】調節自在なルーレットを実装

調節自在なルーレットのサンプル画像

エディタ拡張を使用してゲーム実行前に設置出来る、調整自在のルーレットを実装。
インスペクターから、以下の調整が可能。

  • 要素数
  • 要素の色
  • ルーレットのサイズ
  • 針のサイズ
  • 回転速度
  • (停止時の)慣性の長さ
  • 文字フォント
  • 文字サイズ
  • 文字色
  • (要素毎の)表示文章

↓3D版は以下。

サンプル動画

15秒程の短いサンプル動画です。

右側の数字は、針が指している位置のインデックス。
(テスト用なのでコードには含まれない)

素材画像

  • ライセンス: CC0
  • 圧縮済みPNG

画像の設定

  1. Projectウィンドウの任意のフォルダに、ドラッグ&ドロップしてインポート。
  2. 対象画像を選択。
  3. インスペクター -> Texture TypeをSprite (2D and UI)に変更。
  4. Apply
  • *(2Dモードの場合は、インポートした時に自動的に設定されている)

コード

RouletteManager

  1. (TextMeshProを入れていない場合)TextMeshProをインポート。
  2. 新規スクリプトを作成、「RouletteManager」と命名。
  3. 以下のコードを全文コピーして貼り付け。
  4. 任意のCanvasに空のオブジェクトを作成、「Roulette」と命名。
  5. RouletteにRouletteManagerをアタッチ。
    1. インスペクターから、CircleとClapperのSpriteを紐付け。
    2. 各項目を好きなように設定。
    3. Createをクリック。
  6. 回転終了時の結果に応じた処理を追加しておく。


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

public class RouletteManager : MonoBehaviour
{

	[HideInInspector]
	[SerializeField]
	Transform tf;

//ルーレットの本体部分のRectTransform。
	[HideInInspector]
	[SerializeField]
	RectTransform elementsRt;


	[Header("白い円形の画像を紐付け")]
	[SerializeField]
	Sprite circleSprite;

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

	[Header("ルーレットの要素数")]
	[SerializeField]
	[Range(2, 100)]
	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("ルーレットの画像サイズ")]
	[SerializeField]
	[Range(100, 1024)]
	float rouletteSize = 512;

	[Header("ルーレットの針の画像サイズ")]
	[SerializeField]
	[Range(10, 128)]
	float clapperSize = 64;

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


	[HideInInspector]
	[SerializeField]
	float elementRotationToAdd;

	[HideInInspector]
	[SerializeField]
	float elementFillAmount;


//コルーチン管理用。
	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]
	TMP_FontAsset font;

	[Header("要素毎に入れる文字のサイズ")]
	[SerializeField]
	[Range(16, 128)]
	int fontSize = 64;

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

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


//ルーレット生成時に一時的に使用。
	GameObject elementGo;
	RectTransform elementRt;
	Image elementImg;

	GameObject elementTmpGo;
	RectTransform elementTmpRt;
	TextMeshProUGUI elementTmp;



	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) {

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


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

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

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


					if (inertiaDuration <= inertiaElapsedTime) {

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

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

						spin = null;
						yield break;
					}

					yield return null;
				}
			}

			yield return null;
		}
	}


//Createボタンを押すと、各種設定に応じてルーレットと針を自動生成。
	public void CreateRoulette()
	{
		if (circleSprite == null || clapperSprite == null) {
			print("インスペクターから各Spriteを紐付けしてください!");
			return;
		}

		if (tf == null) {
			tf = transform;
		}

		elementRotationToAdd = 360.0f / elementCount;
		elementFillAmount = 1.0f / elementCount;


		elementGo = new GameObject("RouletteElements");
		elementsRt = elementGo.AddComponent<RectTransform>();
		elementsRt.SetParent(tf);
		elementsRt.localScale = Vector3.one;
		elementsRt.anchoredPosition = Vector2.zero;
		elementsRt.sizeDelta = new Vector2(rouletteSize, rouletteSize);

		for (int i = 0; i < elementCount; i++) {
			elementGo = new GameObject(System.String.Format("RouletteElement{0}", i + 1));
			elementRt = elementGo.AddComponent<RectTransform>();
			elementRt.SetParent(elementsRt);
			elementRt.localScale = Vector3.one;
			elementRt.anchoredPosition = Vector2.zero;
			elementRt.sizeDelta = new Vector2(rouletteSize, rouletteSize);


			elementTmpGo = new GameObject(System.String.Format("Tmp{0}", i + 1));
			elementTmpRt = elementTmpGo.AddComponent<RectTransform>();
			elementTmpRt.SetParent(elementRt);
			elementTmpRt.localScale = Vector3.one;
			elementTmpRt.anchoredPosition = new Vector2(0, rouletteSize / 4 + fontSize / 2);
			elementTmpRt.RotateAround(elementsRt.position, Vector3.back, elementRotationToAdd / 2);
			elementTmp = elementTmpGo.AddComponent<TextMeshProUGUI>();

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

			elementTmp.fontSize = fontSize;
			elementTmp.enableWordWrapping = false;
			elementTmp.alignment = TextAlignmentOptions.Center;
//一応、RaycastTargetと、リッチテキストを無効化している(使う場合は戻してください)。
			elementTmp.raycastTarget = false;
			elementTmp.richText = false;


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

			elementRt.Rotate(Vector3.back * (elementRotationToAdd * i));


			elementImg = elementGo.AddComponent<Image>();
			elementImg.sprite = circleSprite;
			elementImg.type = Image.Type.Filled;
			elementImg.fillMethod = Image.FillMethod.Radial360;
			elementImg.fillOrigin = (int)Image.Origin360.Top;
			elementImg.fillAmount = elementFillAmount;
//一応、RaycastTargetを無効化している(使う場合は戻してください)。
			elementImg.raycastTarget = false;

			if (i < elementColors.Length) {
				elementImg.color = elementColors[i];
			}
		}


		elementGo = new GameObject("Clapper");
		elementRt = elementGo.AddComponent<RectTransform>();
		elementRt.SetParent(tf);
		elementRt.localScale = Vector3.one;
		elementRt.anchoredPosition = new Vector2(0, rouletteSize / 2);
		elementRt.sizeDelta = new Vector2(clapperSize, clapperSize);

		elementImg = elementGo.AddComponent<Image>();
		elementImg.sprite = clapperSprite;
	}

}

RouletteManagerEditor

  1. (無い場合は)Editorフォルダを新規作成。
  2. Editorフォルダ内に新規スクリプトを作成、「RouletteManagerEditor」と命名。
  3. 以下のコードを全文コピーして貼り付け。


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();
		}
	}
}

使用手順

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

設定し直す場合

  1. ヒエラルキー -> Rouletteの子のRouletteElements、Clapperを削除。
  2. 再びRouletteManagerで設定して、Createを実行すれば良い。

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