RhinoInside と Unity を使ったゲーム作り
04 July, 2020 - 7 min read - Tags: Unity,RhinoInside,Qiita
はじめに
これは 2020/07/04 に行われたTokyo AEC Industry Dev Group でのハンズオンをもとにした資料になります。 RhinoInside と Unity を使ったボールをゴールへ運ぶゲームのつくり方になります。 参考のデータはこちらの GitHub のリポジトリにあります。
そもそも RhinoInside とは?という方はこちらを参照してください。 Rhinoceros という 3DCAD ソフトの機能をほかのソフトで利用する仕組みになります。
完成品のイメージ
実際に作っていきましょう!!
0. 環境構築
-
RhinoInside のリポをクローンしておく(いくつかのファイルを使う)
- ここからクローン or ダウンロード
-
RhinoWIP
- ここからダウンロード
-
Unity2019.4.1.f1
- ここからダウンロード
-
Rider2020.1 (スクリプトエディタ)
- コードを書くエディタです。VisualStudio や VSCode などでもよいです。
- Unity 関連のデータをダウンロードをしておいてください。
-
Rider の場合
- 特に追加でダウンロードするものはないです
-
Visual Studio の場合
- Visual Studio Installer から以下の Unity に関するものをインストール
-
VS Code の場合
- Extensions から Debugger for Unity をインストール
-
エディタの設定は Unity の以下から設定
1. Unity で Rhino を使う
1.1 RhinoInside を起動できるようにする
- Asset 下に Scripts という名前のフォルダを作成してそこに"Convert.cs"を入れる。"LoftSurface.cs"を作る
-
Asset 下に Plugins という名前のフォルダを作成してそこに"RhinoCommon.dll"を入れる。
- Convert.cs、RhinoCommon.dll はクローンしたリポの中に入っています。
-
Asset 下に Editor というフォルダを作成して、"RhinoInsideUI.cs"を作る
- Editor という名前のフォルダ名は特殊な扱いを受けるのでフォルダ名は間違えないで入れてください。
- 以下を書いて、エディタから Rhino を起動してみる
using System;
using System.IO;
using UnityEngine;
using UnityEditor;
using Rhino.Runtime.InProcess;
[ExecuteInEditMode]
public class RhinoInsideUI : MonoBehaviour
{
[MenuItem("Rhino/Start RhinoInside")]
public static void StartRhinoInside()
{
string rhinoSystemDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Rhino WIP", "System");
var path = Environment.GetEnvironmentVariable("PATH");
Environment.SetEnvironmentVariable("PATH", path + ";" + rhinoSystemDir);
GC.SuppressFinalize(new RhinoCore(new string[] { "/scheme=Unity", "/nosplash" }, WindowStyle.Minimized));
}
}
1.2 Rhino で Surface を作る
-
ロフトサーフェスを作る
- まずは Rhino 内で作ってみる。
1.3 RhinoInside で Surface を作る
-
次に RhinoInside を使って作ってみる。
- コントロールポイントをまず作る
public class RhinoInsideUI : MonoBehaviour
{
public static void StartRhinoInside()
{
// 省略
}
[MenuItem("Rhino/Create Loft Surface")]
public static void Create()
{
var surface = new GameObject("Loft Surface");
surface.AddComponent<LoftSurface>();
CreateLoft(surface);
}
private static void CreateLoft(GameObject surface)
{
surface.AddComponent<MeshFilter>();
// Surfaceの色の設定
var material = new Material(Shader.Find("Standard"))
{
color = new Color(1.0f, 0.0f, 0.0f, 1.0f)
};
surface.AddComponent<MeshRenderer>().material = material;
// 影を落とさないようにする
surface.GetComponent<MeshRenderer>().receiveShadows = false;
// コントロールポイントの色の設定
var cpMaterial = new Material(Shader.Find("Standard"))
{
color = new Color(0.2f, 0.2f, 0.8f, 1f)
};
// コントロールポイントの作成
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
int num = 4 * i + j;
var controlSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
controlSphere.name = "Sphere" + num;
controlSphere.transform.parent = surface.transform;
controlSphere.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
controlSphere.transform.position = new Vector3( i * 5f, 0, j * 5f);
controlSphere.GetComponent<MeshRenderer>().material = cpMaterial;
}
}
}
}
作ったコントロールポイントを使ってロフトサーフェスを作る
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Rhino.Geometry;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if UNITY_EDITOR
[ExecuteInEditMode]
#endif
public class LoftSurface : MonoBehaviour
{
void Update()
{
var controlPoints = new List<List<Vector3>>();
int i = 0;
List<Vector3> controlPointsRow = null;
foreach (UnityEngine.Transform controlSphere in transform)
{
if ((i++ % 4) == 0)
{
controlPointsRow = new List<Vector3>(4);
controlPoints.Add(controlPointsRow);
}
controlPointsRow.Add(controlSphere.position);
}
gameObject.GetComponent<MeshFilter>().mesh = CreateLoft(controlPoints);
}
private UnityEngine.Mesh CreateLoft(List<List<Vector3>> controlPoints)
{
if (controlPoints.Count > 0 )
{
var profileCurves = new List<Curve>();
foreach (var controlPointsRow in controlPoints)
{
profileCurves.Add(Curve.CreateInterpolatedCurve(controlPointsRow.ToRhino(), 3));
}
Brep brep = Brep.CreateFromLoft(profileCurves, Point3d.Unset,Point3d.Unset, LoftType.Normal, false)[0];
Rhino.Geometry.Mesh mesh = Rhino.Geometry.Mesh.CreateFromBrep(brep, MeshingParameters.Default)[0];
return mesh.ToHost();
}
return null;
}
}
これで"Rhino/Start RhinoInside" をした後に、"Rhino/Create Loft Surface"を押すとロフトサーフェスが作成されるはずです。
ここまでの内容は、part1 のフォルダのデータになっています。
1.4 Unity のデバッグの仕方
- Unity にエディタをアタッチすることでデバッグできます
これで RhinoInside は終わり。あとは Unity のみになります。
2. ゲーム化する
2.1 ボールを弾ませる
- Ball を作成する
-
play▶ してみる
- 何も行らない…
-
RigidBody をアタッチする
- 重力で落ちていくが貫通する…
-
LoftSurface に MeshCollider をアタッチする
- Ball が弾まない…
-
Materials フォルダを作成してそこに PhysicMaterial を作成する。
- Bounces を任意の値にして、Ball と LoftSurface にアタッチする。
- ボールが弾む!!
-
コントロールポイントを動かしてみる
- コライダーが反映されない…
-
動的に MeshCollider をアタッチできるようにする。
- LoftSurface.cs に以下を追記
- アタッチされているゲームオブジェクトに MeshCollider があれば削除し、新しい MeshCollider を設定する SetMeshCollider メソッドを追加している
public class LoftSurface : MonoBehaviour
{
void Start()
{
SetMeshCollider(gameObject);
}
private void SetMeshCollider(GameObject obj)
{
if (obj.GetComponent<MeshCollider>() != null)
{
DestroyImmediate(gameObject.GetComponent<MeshCollider>());
}
obj.AddComponent<MeshCollider>();
obj.GetComponent<MeshCollider>().material = new PhysicMaterial("SurfMaterial")
{
bounciness = (float) 1.0
};
}
void Update()
{
SetMeshCollider(gameObject);
var controlPoints = new List<List<Vector3>>();
// 以下省略...
2.2 ゲームオーバー時にゲームを再スタートできるようにする
- SampleScene の名前を GameScene に変える
-
Cube を作成する
- 名前を Respawn にする
-
LoftSurface の下の方に適当な距離をとって、X と Z の Scale を 100 にする
- ここに当たらないと再スタートしないので、位置に注意
- リスポンの判定に使うのみで、レンダーする必要なので MeshRenderer を非アクティブにする
-
Add Component で Respawn.cs を追加する。
- コライダーに入ってきたら実行するメソッド OnCollisionEnter を使う
- シーンを読み込む形で再スタートを実装する
using UnityEngine;
using UnityEngine.SceneManagement;
public class Respawn : MonoBehaviour
{
private void OnCollisionEnter(Collision other)
{
SceneManager.LoadScene("GameScene");
}
}
2.3 ゴールを作る
-
Cube で作成する
- 名前を Goal にする
-
ゴールにしたい個所に配置する
- スケールも好きな値に設定する
- 単純にこれがゲームの難しさになるので注意
- Add Component で Goal.cs を追加する。
- ゲームクリア時の画面を作成(次のところでまとめて作成するので後回し)
-
ゲームクリアなので Ball を消す
- SerializeField をつけるとエディタ上から値を設定できるようになる
- Ball をエディタ上で設定する
public class Goal : MonoBehaviour
{
[SerializeField] private GameObject ball;
private void OnCollisionEnter(Collision other)
{
ball.SetActive(false);
}
}
2.4 現状の確認
ここまで作ると Unity はこんなになっているはずです
![]https://raw.githubusercontent.com/hrntsm/RhinoInsideGame/master/images/GameScene.png)
ここまでのデータは part2 のフォルダ に入っているものになっています
3. UI を作っていく
3.1 クリア画面を作る
-
Hierarchy で右クリックして、UI から Text を選ぶと Hierarchy に Canvas と EventSystem と Canvas の子に Text が作成される
- Canvas のサイズは Game ウインドウのサイズによるので注意してください
- Text にクリアを示す文字を入れる
- Panel を使って背景を入れる
- Panel の名前を GoalPanel にして、Text を子にする
- GoalPanel を非アクティブにする
-
2.3 で作成した Goal.cs に下記を追記して、Ball が Goal に入った時に GoalPanel をアクティブにして表示されるようにする
- エディタから GoalPanel をセットしておく
public class Goal : MonoBehaviour
{
[SerializeField] private GameObject ball;
[SerializeField] private GameObject goalPanel; // 追加
private void OnTriggerEnter(Collider other)
{
goalPanel.gameObject.SetActive(true); // 追加
ball.SetActive(false);
}
}
3.2 リスポンの確認画面を作成する
- 3.1 と同様に Text と Panel を使って確認画面を作成する
-
2.2 で作成した Respawn.cs を以下のように書き換える
- Ball が Respawn の枠内に入ったらボールを消して、リスポン確認画面を表示させる
- Update では_retry が true かつ右クリックが押されたら GameScene をロードさせる
public class Respawn : MonoBehaviour
{
[SerializeField] private GameObject ball;
[SerializeField] private GameObject respawn;
private bool _retry = false;
private void OnCollisionEnter(Collision other)
{
respawn.SetActive(true);
ball.SetActive(false);
_retry = true;
}
void Update ()
{
if (Input.GetMouseButtonDown (0) & _retry == true)
{
SceneManager.LoadScene("GameScene");
}
}
}
3.3 コントロールポイントの座標をスライダーで変更できるようにする
- UI から Slider を作成する
- Anchors を左の中央にする
- Slider の MinValue を-10、MaxValue を 10 にする
- MoveSphere.cs を作成して Slider にアタッチする
public class MoveSphere : MonoBehaviour
{
[SerializeField] private GameObject sphere;
private Slider _slider;
private void Start()
{
_slider = gameObject.GetComponent<Slider>();
_slider.value = 0;
}
public void Move()
{
// gameobject.transform.position.y は値が変えられないのでいったんposを介して値を変える
var pos = sphere.transform.position;
pos.y = _slider.value;
sphere.transform.position = pos;
}
}
- sphere の部分に座標を操作したいコントロールポイントを設定する
-
Slider の On Value Changed を設定する
- ここで設定されたものはスライダーの値が変えられたときに呼ばれる
- 各コントロールポイントにスライダーを設定する
3.4 カメラを設定する
- MainCamera を選択すると Scene のウインドウの中にカメラのビューが表示される
-
ゲーム画面にしたい、いい感じのアングルを設定する
3.5 ゲームのスタート画面を作る
-
Project ウインドウを右クリックして Create から Scene を作成する
- 名前は TitleScene とする
- Scene を TitleScene に切り替える
-
Respawn 画面などでやったように Title 画面を作成する
-
Create Empty から空の GameObject を作り、それに TitleSceneScript をアタッチする
- 今は Unity エディタから RhinoInside を起動しているが、ビルドした単体のアプリとしても RhinoInside を起動できるようにしなければいけないので、Start に RhinoInside を起動する部分をかく
- Update には画面をクリックしたら先程まで作っていた GameScene がロードされるようにしている
using System;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
using Rhino.Runtime.InProcess;
public class TitleSceneScript : MonoBehaviour
{
private void Start()
{
string RhinoSystemDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Rhino WIP", "System");
var PATH = Environment.GetEnvironmentVariable("PATH");
Environment.SetEnvironmentVariable("PATH", PATH + ";" + RhinoSystemDir);
GC.SuppressFinalize(new RhinoCore(new string[] { "/scheme=Unity", "/nosplash" }, WindowStyle.Minimized));
}
void Update () {
if (Input.GetMouseButtonDown (0)) {
SceneManager.LoadScene("GameScene");
}
}
}
4. ゲームとしてビルドする
- File - Build Settings を開く
- Scene In Build で作成した 2 つのシーンを登録する
- Architecture は x86_64 にする(多分デフォルトでこのあたい)
- PlayerSettings から OtherSettings から ScriptingBackend を Mono、Api Compatibility Level を .Net 4.x にする
- Build する
- 完成!!!!!
5. その他
-
Build Setting で Build したものでもスクリプトのデバッグできる設定がある
-
起動時に RhinoInside の起動の待ちが気になる
- 該当箇所を非同期処理に書き換える
- Task.Run を使って非同期化
- うまく CancellationToken を設定できてないので、強制終了しかできない…
public class TitleSceneScript : MonoBehaviour
{
void Start()
{
string RhinoSystemDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Rhino WIP", "System");
var PATH = Environment.GetEnvironmentVariable("PATH");
Environment.SetEnvironmentVariable("PATH", PATH + ";" + RhinoSystemDir);
Task.Run(() =>
new RhinoCore(new string[] {"/scheme=Unity", "/nosplash"}, WindowStyle.Minimized));
}
}
まとめ
- 最終版は final version のものになっています。
- ほとんど Unity でしたが、うまく動きましたでしょうか。
- RhinoInside の部分は、RhinoInside のリポの Unity フォルダの sample1 のものを参考にしています。