小嶋秀樹 | 研究室
日本語 | English
Unity 入門編

Unity の基本的な使い方を,Unity 本家のチュートリアルビデオ を通して学んでいきます.英語の練習をしたい人は,このビデオを見ながら進めてください.

ここでは Unity 5.5 を使います.Unity 本家のウェブサイトからダウンロードして,インストールしてください.ディスクスペースに余裕があれば,iOS 版も併せてインストールしておくとよいでしょう.

Roll-a-ball その1
【新規プロジェクトの作成】

ここでは,平面上でボールを転がすシンプルなゲーム Roll-a-ball の制作をとおして,Unity の基本を学んでいきます.

まずは Unity を起動し,新しいプロジェクト(uniTutorial)をつくります."3D" を選択し,管理しやすい保存場所を指定してください.ここでは,パフォーマンス測定のための Unity Analytics はオフとしておきましょう.準備ができたら Create project を押します.

図

すると,つぎのような Unity 開発ウィンドウが開きます.初期状態では,上端にツールバー,中央に Scene ビュー(神の視点・カメラの視点),その左側に Hierarchy ウィンドウ(オブジェクトの階層),右側に Inspector ウィンドウ(オブジェクトの設定)があります.いまシーンビューには「神の視点」からみた「世界」が表示されています.何も目立ったものはありません.Unity でのプログラミングとは,この空っぽの「世界」にさまざまなオブジェクトやそれらの間の相互作用をつくりだしていくことです.

最初に,この空っぽのシーンを保存します.File > Save Scenes です.ここでは,uniTutorial0/Assets/ の下に "_Scenes" という新規フォルダを作り,その中に "MiniGame" という名前で保存します.ウィンドウ左端の Hierarchy ウィンドウに "MiniGame" という Scene が見えます.メインカメラと照明(Directional Light: 太陽光のように無限遠のある方向からの光)が入っています.

【シーンに「地面」オブジェクトを導入する】

このシーン(MiniGame)にオブジェクト(大道具・小道具・登場キャラクタなど)を導入します.まずは「地面(Ground)」です.GameObject > 3D Object > Plane(平面)を選択してください.平面が導入されます.Hierarchy ウィンドウには "Plane" というオブジェクトが追加されます.これをクリックして,"Ground" という名前に変更しておきましょう.

Mac 上の Unity であれば,二本指スクロールで「拡大縮小」,二本指ドラッグで「視線方向の回転」,alt+一本指ドラッグで「原点回りに回転するように視点移動」などのように,シーン画像の撮影方向などを制御することができます.Fキーを押せば,オブジェクトがよく見えるようにシーン画像が調整されます.(それでも "Ground" を見失ってしまった場合は,たぶん視点が地下に潜ってしまったのでしょう.alt+一本指ドラッグで,シーンを上(Y>0)から見るように視点移動してください.平面には表裏があり,裏(Y<0)から見ると何も見えません.)

Hierarchy ウィンドウから "Ground" を選択すると,右側の Inspector にはさまざまな設定情報が表示されます.よく使うのは Transform です.「世界」(あるいは Hierarchy ウィンドウの中で上位のオブジェクト)の中心を基準として,選択されたオブジェクトの位置や姿勢,拡大縮小率などを設定できます.初期状態では,"Ground" は「世界」の原点 (0, 0, 0) に位置しています.

【シーンに「プレーヤ」オブジェクトを導入する】

このゲームの「プレーヤ」はボール(sphere)です.GameObject > 3D Object > Sphere を導入し,"Player" という名前を与えます.”Ground" の中央にボールらしきものが見えます.Hierarchy ウィンドウから "Player" を選択し,Fキーを押すと,ボールがシーン画像の中央で拡大表示されます.光の加減で見づらい場合は,alt+一本指ドラッグで,視点を回転させてみてください.

ボールが平面にめり込んでいるのがわかります.これはボールの初期位置がシーンの原点 (0, 0, 0) になっているためです.ためしにボールを動かしてみましょう."Player" を選択し,緑色の矢じり(Y-軸)を掴んで,上下させてみてください.移動するにつれて,Inspector の Y 座標も変化しています.Inspector 側で数値を入力すれば,その位置にボールが移動します.ここでは,ちょうど平面上にボールが乗るように,Y = 0.5 としましょう.(Unity オブジェクトの多くは,縦横高さが 1x1x1 を初期値としています.)

白い地面に白いボールではちょっと見づらいので,これらオブジェクトに色をつけることにします.色やテクスチャをつけるには Material(画材)を用意する必要があります.ウィンドウ下部にある Project ウィンドウから Create > Folder で新規フォルダを Assets の中につくり,"Materials"(複数形)に名称変更します.この "Materials" フォルダを選択した状態で,Create > Material を選びます.フォルダの中に新規画材 "New Material" が生成されるので,これを "Background"(背景)に名称変更します.これを選択すると,Inspector 側で設定変更ができるようになります.色は Albedo(散乱反射)で設定します.カラーピッカーで適当な色を選択してください.(他にも詳細な設定ができますが,ここでは Albedo の設定のみにしておきます.)

Material をオブジェクトに適用するには,この Material "Background" を,シーン画面中の対象オブジェクト(この場合はボール)にドラッグ&ドロップするだけです.

さらに見やすくするために,照明(Directional Light)の方向を微調整します.Hierarchy ウィンドウから Directional Light を選択し,Inspector で Rotation の Y を 60 にします.自然に見える向きを alt+一本指ドラッグで見つけてください.

Roll-a-ball その2
【いよいよ「プレーヤ」を動かす】

オブジェクトを動かすには「物理演算」が必要になります.地面を転がったり,障害物に衝突したりといった,物理シミュレーションです.そのために,まず,オブジェクト "Player" を「剛体(ごうたい / RigidBody)」として扱えるようにします."Player" を選択してから,Add Component > Physics > Rigidbody を選択します.Inspector を見ると,Rigidbody 項目が加わっています.

ここではキーボード操作によって "Player" に「力」が加わるようにして,"Player" をシーンの中で動かすようにします.これを実現するために,"Player" に簡単な「スクリプト」を与えましょう.Material の場合と同じように,Project ウィンドウの中で Assets フォルダの中に "Scripts" というフォルダをつくり,その中に "C# Script" を新規作成し,"PlayerController" に名称変更しておきます.まだスクリプトはデフォルトのままですが,Material の場合と同じように,シーン画面中のボールにドラッグ&ドロップして,関連づけておきます.

このスクリプト "PlayerController" を編集しましょう.アイコンをダブルクリックして(あるいは Inspector の PlayerController (Script) の設定アイコンから Edit Script を選んで)エディタを開きます.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {
    //  剛体
    private Rigidbody rb;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (force3);
    }
}

ざっと読むと,空っぽの Start() と Update() という関数らしきものがあります.これらは,ちょうど openFramewoks で言うところの setup() と update() に対応するものと考えられます.ここでは,ユーザからの入力(ここでは上下と左右のカーソルキー)を読み取り,ボールへの力学的な作用をつくりだす関数を用意します.そのために用意すべき関数名は FixedUpdate() です.FixedUpdate は,物理演算の直前に実行され,毎フレームごと,あるいはより短い間隔で呼び出されます.また,力を加える対象は Rigidbody なので,初期処理としてボールの Rigidbody を取得しておく必要があります.これは最初に1回だけ実行される初期化関数 Start() に記述します.

これら関数(イベント関数)は,およそ下図のような順序で呼び出されます.まず最初に,すべての Unity オブジェクトについて Start() が実行されます.毎フレームごとのグラフィック描画の直前に,すべてのオブジェクトについて Update() が実行されます.また,物理演算の直前に実行される FixedUpdate() は一定周期で呼び出されるため,フレームレートが遅い場合には,赤い矢印で示すように,複数回連続して呼びだされる場合があります.なお,処理が不要な関数は省略可能です.

この「世界」を動かしてみましょう.ウィンドウ上部にある「再生ボタン」を押してください.

キーの読み取りは FixedUpdate() の中で行っています.水平方向のキー操作は X 軸(地面上の一方向)の力に対応させ,垂直方向のキー操作は Z 軸(地面上で X 軸と直交する方向)の力に対応させます.Unity では,高さ方向が Y 軸となり,地面が X-Z 平面となります.

ちょっとキーによる力が弱いようなので,感度を上げてみます.つぎのスクリプトのように public 変数を用意すると,Inspector から値を調整することができます."sensitivity" という実数変数を用意し,力ベクトルに掛けることで,力を調整できるようにします.Inspector から 3.0 あたりを与えるとよいでしょう.ボールが敷地をはみ出すとどうなりますか?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  剛体
    private Rigidbody rb;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
}
【カメラを動かす】

カメラの見下ろし角が浅すぎるようなので,カメラの位置と姿勢を調整してみます.Hierarchy ウィンドウから Main Camera を選択し,Inspector でその位置を (0, 10, -10) とし,姿勢を (45, 0, 0) とします.つまりカメラを上方向に 10 だけ移動し,その視線方向を X 軸回り(つまりピッチ方向)に 45 度だけ下向きに回転します.Camera Preview に新しいカメラから見た情景が表示されています.

この「世界」を動かせば,Main Camera から見た情景でゲームできます.少しはリアルな感じが出るようになりました.

つぎに,この Main Camera を "Player" の「子供」にしてみます.Hierarchy ウィンドウの中で,Main Camera をドラッグし,"Player" の中に入るように設定します.("Player" の次行に置くのではなく,"Player" の子要素として入れてください.)

この状態で「世界」を動かすとどうなりますか? カメラはボールを中心に捉えていますが,ボールが転がるとともに画像も回転していきます.カメラはボールを中心に (0, 10, -10) の位置に,(-45°, 0°, 0°) の姿勢を保ちます.ボールは地面を「転がって」いるので,カメラの位置も転がるボールを中心として回転しているわけです.Game 画像のタブを動かして,Scene 画像と Game 画像が横並びに表示されるように配置すると,この不思議なカメラの動きをよく観察することができます.

この回転を止め,カメラが同じ姿勢を保ちながらボールを追いかけるようにするには,どうすればよいでしょうか.まずは Main Camera を "Player" の子供から外して,元に戻します.そして,つぎのようなスクリプト "CameraController" を Main Camera に与えます.Hierarchy ウィンドウから Main Camera を選択し,Inspector の下部にある Add Component を押し,New Script (C Sharp) として "CameraContoller" を新規作成します.Asset のトップに生成されるので,Scripts フォルダの中に移動しておくとよいでしょう.移動した "CameraContoller" をエディタで開き,つぎのように編集します.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour {
    //  ボール "Player" への参照(Inspector で要設定)
    public GameObject player;
    //  ボールからカメラへの相対オフセット
    private Vector3 offset;
    //  初期処理:オフセット量の計算
    void Start () {
        //  transform.position はカメラ自身の位置
        //  player.transform.position はボールの位置
        offset = transform.position - player.transform.position;
    }
    //  毎フレームごとの処理:カメラの移動
    void Update () {
        //  ボールの位置にオフセットを加えればカメラ位置が決まる
        transform.position = player.transform.position + offset;
    }
}

赤字で書いた "Player" への参照は,Inspector 上で値を設定します.Hierarchy ウィンドウで Main Camera を選択すると,Inspector 上に未設定の参照欄 Player が見えるはずです.この状態で,Hierarchy ウィンドウの "Player" をドラッグして,この参照欄にドロップします.これで,"CameraContoller" から "Player" の動きを参照できるようになります.

【周囲に壁をつくる】

敷地の周囲に「壁(walls)」をつくり,ボールが落ちないようにしましょう.まず,敷地を広げます.Hierarchy ウィンドウから "Ground" を選択して,Scale を (2, 1, 2) としてください.水平面(X-Z 平面)上で,2倍(面積は4倍)に拡大されます.

つぎに,GameObject メニューから Create Empty を選択し,空(汎用)のオブジェクトを生成します.Hierarchy ウィンドウに GameObject という項目が現れますので,それを "Walls" という名前に変更します.また,この "Walls" について,Inspector 上で Transform を原点に Reset します.歯車アイコンを押して Reset を選択してください.これで原点 (0, 0, 0) に位置するようになります.

つぎに壁を1枚つくります.GameObject から 3D Object > Cube を生成し,それを "Walls" の子要素に入れてください.名前は "WestWall" に変更します.Inspector 上で Scale を (0.5, 2.0, 20.5) として壁状にし,Position を (−10, 0, 0) とします.これで「西壁」ができました.

この「西壁」をコピーして「東壁」をつくります.Hierarchy ウィンドウの "WestWall" を選択してから,Edit > Duplicate(複製)を選びます.これで "WestWall (1)" が同じ位置に複製されました.この位置を Inspector 上で (10, 0, 0) に変更します.これで「東壁」もできました.

さらにコピーして「北壁」をつくります.「東壁」を Duplicate したら,Y 軸回りに 90° 回転させ,Position を (0, 0, 10) とします.

最後は「南壁」です.「北壁」を Duplicate し,Position を (0, 0, −10) とします.これで "Walls" の子要素として4枚の壁ができました.

早速「再生ボタン」を押して,この「世界」を動かしてみましょう.いかがですか? 壁によってボールの動きが制約されているのがわかります.ボールの回転を観察するには,Hierarchy ウィンドウから "Player" を選択し,ウィンドウ上部にあるボタン Global を押して Local に変更してから,再び「世界」を動かします.ボールのローカル座標軸が表示され,回転していく様子が見られます.

「壁」に当たると反射はしないようですね.いわゆる非弾性衝突です.本家チュートリアルの範囲を越えますが,この「壁」に跳ね返りの性質を与えましょう.Project ウィンドウの中の Materials フォルダに,Physics Material を新規作成します.これはオブジェクトの衝突時の物理特性を決めるものです.名前を "Bouncing" とし,Inspector で Dynamic Friction(動摩擦係数),Static Friction(静止摩擦係数),Bounciness(反発度)をそれぞれ 0.1, 0.2, 0.9 としましょう.

この "Bouncing" を4枚の壁と "Player" にドラッグ&ドロップで与え,「世界」を動かしてみてください.壁でボールが反射するようになり,よりリアルな「世界」に感じられませんか.

Roll-a-ball その3
【エサをつくる】

ゲームっぽくするために,"Player" が転がりながらゲットすべき「エサ」をつくります.「エサ」は立方体(cube)です.Hierarchy ウィンドウ上部から Create > 3D Object > Cube を生成し,"PickUp" という名前を与えます.Inspector の Transform にある歯車アイコンから Reset を選択し,原点に位置するようにします."Player" と重なり見づらいので,一時的に "Player" を非アクティブにします.Hierarchy ウィンドウから "Player" を選択し,Inspector 上部にあるチェックを外してください.これで "PickUp" だけが中央に見えるはずです.

Fキーを押して "PickUp" を拡大しましょう."PickUp" は幅・高さ・奥行きがすべて 1 の立方体ですが,下半分が「地面」に埋め込まれています.ここでは,Position の Y 座標に 0.5 を与え,スケールを (0.5, 0.5, 0.5) として,宙に浮いた小さな立方体にます.さらに,Rotation として (45, 45, 45) を与えて,目立つ存在にします.

より魅力的な「エサ」に見せるために,この "PickUp" を回転させることにします.そのためにはスクリプトが必要となります."Player" の場合と同じように,Inspector 下部にある Add Component を押し,メニューの最下段にある New Script を選択し,新規 C Sharp スクリプトとして "Rotate" を生成します.Project ウィンドウのルートに C# アイコンの "Rotate" が生成されているので,これを Scripts フォルダに移動します.この "Rotate" をつぎのように編集します.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rotate : MonoBehaviour {
    // Update is called once per frame
    void Update () {
        Vector3 rotSpeed = new Vector3(15, 30, 45);
        transform.Rotate(rotSpeed * Time.deltaTime);
    }
}

このスクリプト "Rotate" では,毎フレームごとにオブジェクトの各軸を少しずつ回転させていきます.X 軸は毎秒 15°,Y 軸は毎秒 30°,Z軸は毎秒 45° としています.Time.deltaTime は直前のフレームから現在のフレームまでの時間間隔です.3軸の回転速度をベクトルにしたものに Time.deltaTime を掛けることで,直前のフレームからの回転量を計算しています.早速,再生ボタンを押して,"PickUp" が回転するのを確認してください.

【"PickUp" をプレハブ化する】

"PickUp" を1つ作成しましたが,これと同じものを多数生成し,シーンの中に配置していく必要があります.そのために利用するのが「プレハブ(Prefab)」という仕組みです.まずは,Project ウィンドウの中で Assets フォルダの中に "Prefabs" というフォルダを生成し,その中に Hierarchy ウィンドウから "PickUp" をドラッグ&ドロップしてください.

この Prefab から多数の "PickUp" を生成・配置しますが,そのための受け皿として,Hierarchy ウィンドウに GameObject として "PickUpCircle" をつくります.Hierarchy ウィンドウ上部の Create から Create Empty で空オブジェクトを生成し,その名前を "PickUpCircle" とします.そして,Hierarchy ウィンドウにある "PickUp" の Position が (0, 0, 0) であることを確かめて,この "PickUp" を "PickUpCircle" に入れます.

いよいよプレハブの量産に入ります.まず,"PickUpCircle" の子要素となった "PickUp" を選択します.つぎに,シーン画像右上にある投影方向アイコンから Y 軸(緑)を選び,真上からの投影に切り替えます.敷地全体が見えるまで縮小してください.

ウィンドウ上部で Global 座標系が選択されていることを確認し,中央にある "PickUp" を,下図のように,およそ 12時の位置に移動させます.シーン画像上で "PickUp" をクリックし,水平面を表す正方形をドラッグすればよいでしょう.

つぎに,この "PickUp" を選択した状態で,Edit > Duplicate(または Command-D)で複製をつくります."PickUp (1)" という複製が "PickUpCircle" の子要素として生成されますが,複製元と完全に重なった状態になっています.そこで,"PickUp (1)" が選択された状態で,およそ 1時の方向にドラッグします.

このような操作を繰り返すことで,およそ 30° ごとに 12個の "PickUp" を生成・配置することができました.再生ボタンを押して,この "PickUpCirlce" が動くことを確かめてください.

より魅力的な「エサ」に見えるように,金色に輝く Material を与えましょう.Project ウィンドウで Materials の中に "Gold" という Material を新規作成します.少し赤みがかった黄色を Albedo に設定し,さらに Metalic の値を 0.5 前後に上げてみます.金色に近くなったのではないでしょうか.この "Gold" を Prefabs の中の "PickUp" に適用します.そのために,まず Project ウィンドウの右上にある設定アイコンから "One Column Layout" を選択し,Prefabs フォルダを開いた中にある "PickUp" に "Gold" をドラッグ&ドロップします.これで,12個すべての "PickUp" が金色になったはずです.

【エサとの衝突を検知する】

つぎの段階では,"Player" がエサと衝突することで,エサを獲得できるようにします.まずは Hierarchy ウィンドウから "Player" を選択し,Inspector 上でアクティブにします.この状態で再生ボタンを押せば,"Player" の動きがエサで制約されるのがわかります.

この "Player" のスクリプトに「エサ」との衝突処理を組み込みます.Hierarchy ウィンドウから "Player" を選択し,Inspector 上の "Player Controller (Script)" の歯車アイコンから Edit Script を選択して,スクリプトをエディタで開きます.すでに Start() と FixedUpdate() は定義されています.ここに新しく OnTriggerEnter() を加え,衝突時の処理を記述します.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  剛体
    private Rigidbody rb;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
    //  何かに衝突したときに実行
    void OnTriggerEnter(Collider other) {
        //  相手が "PickUp" ならば非アクティブ化(「エサ」獲得)
        if (other.gameObject.CompareTag ("PickUp")) {
            other.gameObject.SetActive (false);
        }
    }
}

OnTriggerEnter() は何らかのオブジェクトに衝突したときに実行される関数です.引数には,相手オブジェクトの Collider(衝突判定器)です.ただし,壁も地面も衝突相手となりうるので,相手が「エサ」("PickUp")である場合に限り,その「エサ」を非アクティブにして消すようにしています.

衝突相手のタグ(tag)が "PickUp" であるかを判断基準に使っていますが,このタグを改めて Prefab である "PickUp" に与える必要があります.Prefabs の "PickUp" を選択すると,Inspector の上部には Tag として Untagged(タグなし)となっているので,Add tag... から+ボタンを押し,"PickUp" というタグを新規作成します.Prefabs から PickUp を選び直し,新規作成されたタグ "PickUp" を Inspector 上で設定してください.また,Inspector の Box Collider の部分にある Is Trigger を有効(チェック有)にしてください.衝突時に OnTrigger で始まる関数が実行されるようにするには,単なる Collider ではなく Trigger Collider にする必要があるためです.

再生ボタンを押し,カーソルキーでボールを適切に制御すれば,衝突したエサを「ゲット」しているのがわかります.試してみてくださ.

Roll-a-ball その4
【スコア計算のスクリプトを組み込む】

「エサ」をゲットできるようになったので,それをスコアとして蓄積できるようにします.つぎのように,"PlayerContoller" スクリプトに,プライベート変数 score を用意し,その初期化とカウントアップの処理を組み込みます.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  剛体
    private Rigidbody rb;
    //  獲得したエサ
    private int score;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
        score = 0;
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
    //  何かに衝突したときに実行
    void OnTriggerEnter(Collider other) {
        //  相手が "PickUp" ならば非アクティブ化(「エサ」獲得)
        if (other.gameObject.CompareTag ("PickUp")) {
            other.gameObject.SetActive (false);
            score++;
        }
    }
}

このスコアを画面表示するには,Text 要素を導入します.Hierarchy ウィンドウの中でトップの MiniGame を選択した状態で,Create から UI > Text を選択します.Hierarchy ウィンドウには Canvas という親要素の中に Text が生成され,Game 画像の中央に "New Text" という文字が表示されています.Canvas の中の Text 要素を "ScoreText" に名称変更しましょう.ユーザインタフェースに関係する要素は,すべて Canvas の中に入れておくのが原則です.また Canvas に加えて,EventSystem も生成されているのがわかります.

"ScoreText" が画像中央に表示されるのは都合悪いので,Game 画像の左上に比較的明るい文字色で表示されるようにしましょう.Hierarchy ウィンドウから "ScoreText" を選択し,Inspector 上の Rect Transform にある図形をクリックします.いくつかの選択肢が示されまずが,ここではピボット(回転やスケール変換の中心座標)とアンカー(位置計算の基点座標)の両方を「左上端」とするので,shift キーと alt キーを押しながら,左上端(top/left)のアイコンを選んでください.これで "New Text" の表示が Game 画像の左上端となりました.画像端からのスペースが少し欲しいので,相対位置として Pos X に 10 を,Pos Y に −10 を与えましょう.

【スコア表示のスクリプトを組み込む】

表示する準備ができたので,"PlayerController" スクリプトにスコア表示の処理を組み込みます.スクリプト冒頭部分に UnityEnging.UI 名前空間を導入することで,テキスト表示などのユーザインタフェースに関する機能を使えるようになります.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEnging.UI;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  ユーザインタフェース
    public Text scoreText;
    //  剛体
    private Rigidbody rb;
    //  獲得したエサ
    private int score;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
        score = 0;
        scoreText.text = "Score: " + score.ToString();
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
    //  何かに衝突したときに実行
    void OnTriggerEnter(Collider other) {
        //  相手が "PickUp" ならば非アクティブ化(「エサ」獲得)
        if (other.gameObject.CompareTag ("PickUp")) {
            other.gameObject.SetActive (false);
            score++;
            scoreText.text = "Score: " + score.ToString();
        }
    }
}

まず,Text 型の public 変数として scoreText を導入します.後に Inspector から実際の ScoreText オブジェクトを設定します.初期処理として "Score: 0" を表示する処理と,エサを獲得するごとに,新しい score の値でこの表示を更新する処理が加わっています.つぎに,Hierarchy ウィンドウから "Player" を選択し,Inspector 上の Player Controller (Script) に新しく現れた Score Text の欄に,Canvas の中から "ScoreText" をドラッグ&ドロップしてください.これでスクリプト内の scoreText と Canvas/ScoreText が関連づけられました.

それでは再生ボタンを押して,この「世界」を動かしてみてください.エサを獲得するたびにスコアが1ずつ増えていくのがわかります.

せっかくなので,スクリプトを整理しましょう.scoreText の更新処理が全く同じ形で2箇所に現れています.これはあまり良くないので,つぎのように独立した関数 SetScoreText() として括り出します.表示方法を変えたければ,この SetScoreText() だけを変更すればよくなり,ソフトウェアの保守管理がしやすくなります.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEnging.UI;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  ユーザインタフェース
    public Text scoreText;
    //  剛体
    private Rigidbody rb;
    //  獲得したエサ
    private int score;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
        score = 0;
        SetScoreText();
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
    //  何かに衝突したときに実行
    void OnTriggerEnter(Collider other) {
        //  相手が "PickUp" ならば非アクティブ化(「エサ」獲得)
        if (other.gameObject.CompareTag ("PickUp")) {
            other.gameObject.SetActive (false);
            score++;
            SetScoreText();
        }
    }
    //  スコア表示の更新
    void SetScoreText() {
        scoreText.text = "Score: " + score.ToString();
    }
}
【ゲーム終了の判定とメッセージ表示】

すべてのエサ(ここでは 12個)を獲得したら,ゲーム終了のメッセージを表示するようにします.scoreText と同じように,finishText という public 変数とその処理をスクリプトに埋め込みます.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEnging.UI;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  ユーザインタフェース
    public Text scoreText;
    public Text finishText;
    //  剛体
    private Rigidbody rb;
    //  獲得したエサ
    private int score;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
        score = 0;
        SetScoreText();
        finishText.text = "";
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH = Input.GetAxis ("Horizontal");
        float forceV = Input.GetAxis ("Vertical");
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
    //  何かに衝突したときに実行
    void OnTriggerEnter(Collider other) {
        //  相手が "PickUp" ならば非アクティブ化(「エサ」獲得)
        if (other.gameObject.CompareTag ("PickUp")) {
            other.gameObject.SetActive (false);
            score++;
            SetScoreText();
        }
    }
    //  スコア表示の更新
    void SetScoreText() {
        scoreText.text = "Score: " + score.ToString();
        if (score >= 12) {
            finishText.text = "Finish!";
        }
    }
}

また,scoreText と同じように,Canvas に新しい Text を生成し,名前を "finishText" とします.色は白,フォントサイズは 28 程度がよいでしょう.また表示を中央・センタリングとし,縦横方向の割付処理はいずれも Overflow(はみだし)とします.Pos Y に 40 程度のオフセットを与えて,"Player" のやや上側に表示されるようにしましょう.この "finishText" を "PlayerController" の Finish Text の欄にドラッグ&ドロップすれば完成です.

再生ボタンを押して,このゲームを動かしてみてください.すべてのエサを獲得すると,"Finish!" の表示が出ます.これで,だいぶゲームらしくなったと思います.

Roll-a-ball その5
【アプリとしてビルドする】

最後に,独立した「ゲームアプリ」としてビルドします.まずは File > Save Scenes でシーンを保存してから,File > Build Settings... を開きます.Scenes in Build の中に,これまでに作成してきた MiniGame というシーンを入れます.ここに Project ウィンドウにある _Scenes の中の MiniGame をドラッグ&ドロップするか,あるいは MiniGame が開いている唯一のシーンであれば Add Open Scene のボタンを押せばよいでしょう.

ここでは PC, Mac & Linux Standalone を選択します.細かい設定はデフォルトのままで大丈夫です.選択されたプラットフォームには Unity アイコンの付きます.プラットフォームを変更する場合は,下部にある Switch Platform を押してください.すると保存先を聞いてきますので,"Builds" という新規フォルダを作成し,その中に "Roll-a-ball-mac" のように名前をつけます.

ビルドに少し時間がかかりますが,終了すると "Builds" フォルダの中に "Roll-a-ball-mac.app" というアプリケーションが生成されます.これは MiniGame を実行するために必要なすべてがパッケージされた「アプリ」です.いかがですか? フルスクリーンで MiniGame が動くはずです.

この「アプリ」を実行するには,Finder から Roll-a-ball-mac.app をクリックし,実行するだけです.やってみてください.アプリを起動すると,最初に画面解像度と画質などを調整するダイアログが現れます.適切と思われる選択をして,Play ボタンを押してください.

【iOS アプリとしてビルドする】

本家チュートリアルの範囲を越えますが,iOS(iPhone)アプリへのビルドにも挑戦してみましょう.まず,カーソルキーの代わりに iPhone を傾けることで "Player" を動かせるようにします.そのために,"PlayerController" を以下のように修正してください.デスクトップか iPhone 等のモバイルデバイスかを判断し,適切に入力を読み取るようにしています.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEnging.UI;

public class PlayerController : MonoBehaviour {
    //  感度(public 変数は Inspector から調整可能)
    public float sensitivity;
    //  ユーザインタフェース
    public Text scoreText;
    public Text finishText;
    //  剛体
    private Rigidbody rb;
    //  獲得したエサ
    private int score;
    //
    //  最初に1回だけ実行
    void Start()  {
        rb = GetComponent<Rigidbody>();
        score = 0;
        SetScoreText();
        finishText.text = "";
    }
    //  毎フレームごとに物理演算の直前に実行
    void FixedUpdate() {
        //  ユーザ入力から力ベクトルへ
        float forceH, forceV;
        if (SystemInfo.deviceType == DeviceType.Desktop) {
            //  デスクトップの場合
            forceH = Input.GetAxis ("Horizontal");
            forceV = Input.GetAxis ("Vertical");
        } else {
            //  モバイルデバイス(iPhone 等)の場合
            forceH = Input.acceleration.x;
            forceV = Input.acceleration.y;
        }
        Vector3 force3 = new Vector3(forceH, 0.0f, forceV);
        //  力ベクトルを作用させる
        rb.AddForce (sensitivity * force3);
    }
    //  何かに衝突したときに実行
    void OnTriggerEnter(Collider other) {
        //  相手が "PickUp" ならば非アクティブ化(「エサ」獲得)
        if (other.gameObject.CompareTag ("PickUp")) {
            other.gameObject.SetActive (false);
            score++;
            SetScoreText();
        }
    }
    //  スコア表示の更新
    void SetScoreText() {
        scoreText.text = "Score: " + score.ToString();
        if (score >= 12) {
            finishText.text = "Finish!";
        }
    }
}

つぎに,File > Build Settings... から iOS を選び(Switch Platform し),Debug 指定のもとで,つぎのように Player Settings... を(Inspector ビューで)設定します.まず,Company Name と Product Name には適当な名前を入れます.つぎに,iPhone らしきタブを開き,以下のように設定します.(あくまで宮城大学こじ研での設定です.)

これら設定ができたら,Build ボタンを押し,Builds/Roll-a-ball-ios として保存してください.保存されるのは Xcode プロジェクトのファイルツリーです.

保存が完了したら,Xcode から Builds/Roll-a-ball-ios/Unity-iPhone.xcodeproj を開きます.Xcode 上で Unity-iPhone のビルド設定を開き,General タブの中で以下のように設定します.(あくまで宮城大学こじ研での設定です.)

iPhone を USB ケーブルで接続し,Xcode のターゲットをその iPhone に指定します.再生ボタン(Build & Run)を押せば,iPhone に MiniGame アプリがインストールされ,起動するはずです.