エディタ拡張を使用してゲーム実行前に設置出来る、カスタマイズ可能な3Dルーレット。
2D版と違い、ベース部分を可変に出来ないので、画像素材や3Dモデルを多めに用意。
2、3、4、5、6、8、10、12ピースに対応。
2D(UI)版はこちら↓
サンプル動画
- 左側: ワールド空間に2DルーレットVer
- 右側: 3DモデルVer
各パターンの設置&動作テスト
ルーレット結果の精度テスト
画像&3Dモデル素材
- ライセンス: CC0
画像の設定
- Projectウィンドウの任意のフォルダに、ドラッグ&ドロップしてインポート。
- 対象画像を選択。
- インスペクター -> Texture TypeをSprite (2D and UI)に変更。
- Apply
コード
RouletteManager (ワールド空間に2Dのルーレットを設置Ver)
- 要素数を決めて、それに応じたスプライトを紐付けしておく。
- (要素数が6以上の場合は)色配列と、文字配列を追加で設定。
- 各種設定を任意に調整。
- インスペクター最下部の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)
- 要素数を決めて、それに応じた3Dモデルを紐付けしておく。
- (要素数が6以上の場合は)色配列と、文字配列を追加で設定。
- 各種設定を任意に調整。
- インスペクター最下部の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版で共通)
- GameManager等で、RouletteManagerへの参照を保持しておく。
- 回転をスタートさせたい時に、RouletteManagerのStartSpinを呼ぶ。
- 画面タップで徐々に回転が停止。
(Spinコルーチンの終了部分へ、結果に応じた処理を追加しておく)
影が汚い場合
影をなくすパターン
- ルーレットの3Dモデルを選択。
- Mesh Rendererコンポーネント -> shadowCastingModeをOffに設定。
グラフィックの設定を変更するパターン
- Project Settings -> Quality -> Shadow Distanceを低めに調整。