円周率を近似するシミュレーションゲームを一日で作ってみました。
普段はR言語を用いた統計解析を解説していますが、学校の課題でアプリの作成をすることになったので一日で作りました。
一日で作成したのでクオリティに関してはご了承ください。
Pi Simulator
下の画像のような円周率を近似するシミュレーションゲームを作成しました。
今回紹介するゲームは次のURLにて遊べます。
https://sharemygame.com/@usagisan/pi-simulator
右上の数値は円周率の近似値であり、モンテカルロ法によって算出しています。
半径\(1\)の円と一辺の長さが\(2\)の正方形の面積の比は、次のように半径1以内に落ちたボールの数と落としたボールの数の比で近似することができます。
すなわち、円周率\(\pi\)は右辺のボールの数の比の4倍であると近似することができます。
補足ですが、ボールの落ちる座標を\( -1\leq X \leq 1\)、\(-1 \leq Y \leq 1\)であるような二組から成る確率変数\((X, Y)\)とすると、半径\(1\)以内に落ちる確率は
であり、試行回数を増やしていくと先ほどのボールの数の比はこの確率に収束していきます。
自作ゲームの遊び方
遊び方は簡単で、「Enter No. trials...」と書かれた入力欄にシミュレーション回数を入力し、Start ボタンをクリックすることでシミュレーションを開始することができます。シミュレーションを再開したり中断したい場合は、Reset ボタンをクリックしてください。右上にシミュレーションの途中経過(シミュレーションによって得られた円周率, 残りのボールの数、半径 1 の円の中に落ちたボールの数、半径 1 の円の外に落ちたボールの数)がリアルタイムに表示されます。 また、左上の Switch camera をクリックすると、画像2の視点に切り替わり、より臨場感のあるシミュレーションを堪能できます。
スクリプト
あんまり意味はないと思いますが、シミュレーションゲームのスクリプトを載せときます。
次のBall.cs1つ1つのボールの落下位置などを算出するスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Ball : MonoBehaviour { PiSimulationManager simulationManager; [SerializeField] GameObject explosionPrefab; DropBalls dropBalls; [SerializeField] float destroyDelay = 10f; [SerializeField] Color insideColor; [SerializeField] Color outsideColor; // Start is called before the first frame update void Start() { simulationManager = GameObject.Find("SimulationManager").GetComponent<PiSimulationManager>(); dropBalls = GameObject.Find("Dropper").GetComponent<DropBalls>(); } // Update is called once per frame void Update() { } void OnCollisionEnter(Collision collision) { if ((gameObject.transform.position / dropBalls.range).magnitude <= 1) { gameObject.GetComponent<MeshRenderer>().material.color = insideColor; gameObject.GetComponent<MeshRenderer>().material.SetColor("_EmissionColor", insideColor); simulationManager.numberOfBallsInsideCircle++; } else { gameObject.GetComponent<MeshRenderer>().material.color = outsideColor; gameObject.GetComponent<MeshRenderer>().material.SetColor("_EmissionColor", outsideColor); simulationManager.numberOfBallsOutsideCircle++; } var explosion = Instantiate(explosionPrefab, transform.position, Quaternion.identity); explosion.transform.parent = gameObject.transform; Destroy(gameObject, destroyDelay); } } |
DropBalls.csは先ほどのBall.csが付いたボールプレファブをインスタンス化し、落とすためのスクリプトです。入力された試行回数分のボールを射出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class DropBalls : MonoBehaviour { [SerializeField] GameObject ballPrefab; [SerializeField] GameObject ballParent; [SerializeField] float dropingDelay = 0.1f; int simulationTimes; public bool isPressed = false; public float range = 25; // Update is called once per frame void Update() { if (!isPressed) { simulationTimes = FindObjectOfType<PiSimulationManager>().simulationTimes; } if(isPressed && simulationTimes > 0) { StartCoroutine("DropBall"); } } public void StartSimulation() { isPressed = true; } IEnumerator DropBall() { float xCorrdinate = Random.Range(-range, range); float zCorrdinate = Random.Range(-range, range); float dropHeight = gameObject.transform.position.y; GameObject ball = Instantiate(ballPrefab, new Vector3(xCorrdinate, dropHeight, zCorrdinate), Quaternion.identity); ball.transform.parent = ballParent.transform; simulationTimes--; yield return new WaitForSeconds(dropingDelay); } } |
次のPiSimulationManager.csはシミュレーションゲームの全体(半径一以内に落ちたボールの数、外側のボールの数、残りのボールの数)を保存するためのスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class PiSimulationManager : MonoBehaviour { DropBalls dropBalls; public float pi; public int simulationTimes = 100; public int numberOfBallsInsideCircle = 0; public int numberOfBallsOutsideCircle = 0; [SerializeField] InputField inputField; [SerializeField] Text piText; [SerializeField] Text remainigBallsText; [SerializeField] Text ballsInsideText; [SerializeField] Text ballsOutsideText; private void Start() { dropBalls = GameObject.FindObjectOfType<DropBalls>(); } private void Update() { DisplayPi(); } private void DisplayPi() { int i = 0; if (!int.TryParse(inputField.text, out i)) { remainigBallsText.text = "No. remaining balls: ---"; ballsInsideText.text = "No. balls inside: ---"; ballsOutsideText.text = "No. balls outside: ---"; piText.text = "π: ---"; return; } if (int.Parse(inputField.text) <= 0) { remainigBallsText.text = "No. remaining balls: ---"; ballsInsideText.text = "No. balls inside: ---"; ballsOutsideText.text = "No. balls outside: ---"; piText.text = "π: ---"; return; } simulationTimes = int.Parse(inputField.text); remainigBallsText.text = "No. remaining balls: " + (simulationTimes - (numberOfBallsInsideCircle + numberOfBallsOutsideCircle)).ToString(); ballsInsideText.text = "No. balls inside: " + numberOfBallsInsideCircle.ToString(); ballsOutsideText.text = "No. balls outside: " + numberOfBallsOutsideCircle.ToString(); if (numberOfBallsInsideCircle + numberOfBallsOutsideCircle == 0) return; pi = (float) 4 * numberOfBallsInsideCircle / (numberOfBallsInsideCircle + numberOfBallsOutsideCircle); piText.text = "π: " + pi.ToString("f8"); } public void ResetSimulation() { Ball[] balls = GameObject.Find("Balls").transform.GetComponentsInChildren<Ball>(); foreach (Ball ball in balls) { Destroy(ball.gameObject); } pi = 0f; simulationTimes = 100; numberOfBallsInsideCircle = 0; numberOfBallsOutsideCircle = 0; remainigBallsText.text = "No. remaining balls: ---"; ballsInsideText.text = "No. balls inside: ---"; ballsOutsideText.text = "No. balls outside: ---"; piText.text = "π: ---"; dropBalls.isPressed = false; } } |
シミュレーションゲームには全く関係ないですが、以下のスクリプトCameraManager.cs、RotateSun.csはカメラの切り替えや太陽の移動のためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class CameraManager : MonoBehaviour { [SerializeField] Camera mainCamera; [SerializeField] Camera subCamera; Canvas canvas; private void Start() { canvas = GameObject.FindObjectOfType<Canvas>(); subCamera.enabled = false; } public void SwitchCamera() { if (!subCamera.enabled) { subCamera.enabled = true; mainCamera.enabled = false; canvas.worldCamera = subCamera; } else { subCamera.enabled = false; mainCamera.enabled = true; canvas.worldCamera = mainCamera; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class RotateSun : MonoBehaviour { [SerializeField] float rotateSpeed = 3f; float rotationX = 0f; public float rotationRatio = 0f; [SerializeField] Text piText; [SerializeField] Text remainingBallsText; [SerializeField] Text ballsInsideText; [SerializeField] Text ballsOutsideText; void Start() { } // Update is called once per frame void Update() { Vector3 rotationAngles = transform.localRotation.eulerAngles; rotationRatio = (18f + (360f - rotationAngles.x) / 15f) % 24f - 12f; transform.Rotate(new Vector3(0, 1, 0) * rotateSpeed * Time.deltaTime); Color textColor = piText.color; textColor.r = rotationRatio / 12f; textColor.b = rotationRatio / 12f; textColor.g = rotationRatio / 12f; piText.color = textColor; remainingBallsText.color = textColor; ballsInsideText.color = textColor; ballsOutsideText.color = textColor; } } |