tomo_manaのブログ

Unityで一からゲームを作る方法を紹介しています。

Unity学習#12 (Unity 2019.4.1f1) キーの追加(Input Manager と Input System)

今回は、キーの仕様を決定し、キーを追加します。キーの追加には Input Manager か Input System を使用します。Input Manager は従来からあるキー管理の方式、Input Systemは最近追加された方式です。今後は Input System に一本化されるようです。
今回は Input Manager で実装後に、Input System に移植しました。

(今回、かなり長文になりました。Input Systemは明らかに初心者泣かせだと思うので、今後もっと簡単に設定できるようになることを期待します・・・)

キー仕様

今回はステータスとコマンド表示を実装するため、2つのキーを追加します。

  • メニューを追加するために、 "Submit" と "Cancel" を追加します。
  • E に "Submit" を、Q に "Cancel" を 割り当てます。

従来のキー管理 (Input Manager)

Input Manager はプロジェクトで1つ存在していて、オブジェクトを生成するなど特別な作業は不要です。初期設定は不要で、初期状態でのキー配置は以下になっています。

f:id:tomo_mana:20200801001841p:plainf:id:tomo_mana:20200801233235p:plain
Input Manager のキー配置(マウス)

キーの追加

"E"キーを Submit に割り当てるには、以下のようにします。
(1) Unity画面メニューから、Edit > Project Settings... を選択
(Project Settings 画面が表示される)
(2) Input Manager を選択
(3) Axis を選択して展開します
(4) Submit で右クリック > Duplicate Array Element を選択

f:id:tomo_mana:20200806235539p:plain
既存のキーイベントの複製 (Duplicate)

Submit イベントがコピーされる)
(5) Positive Button を "e" に変更(小文字)

f:id:tomo_mana:20200806235504p:plain
E を Submit に割り当てる

Q を Cansel に割り当てる場合は、上記と同じ手順で、Cancel をコピーして Positive Button を "q" に変更します。

動作テスト

キーを受ける処理は、第3回で作成した PlayerControl オブジェクト(キャラクターを十字キーで動かすC#スクリプト)に追加したので、ここに以下を追加します。
コード

public class PlayerControl : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.E)){    // キー取得
            Debug.Log("E");
        };
        if (Input.GetButtonDown("Submit")){    // キーの意味を取得
            Debug.Log("Submit");
        };
        if (Input.GetKeyDown(KeyCode.Q)){    // キー取得
            Debug.Log("Q");
        };
        if (Input.GetButtonDown("Cancel")){    // キーの意味を取得
            Debug.Log("Cancel");
        };
    }
}

新しいキー管理 (Input System)

新しいキー管理の方法 Input System は、プロジェクトで複数のキーマップを保持できます。押されたキー自体の監視(例えば "E" キー自体を監視)には初期設定は不要ですが、キーを意味的に取得する場合(例えば "E" キーやその他のキーをまとめてSubmitとして監視)には一連の初期設定が必要です(本節以降にまとめます)。
第11回 に書いたとおり、複数のゲーム環境に対応するためには、キーを意味的に取得する方法が推奨されています。
Input System を使用するには、1) Input System Asset のインストール、2) Input Manager から Input System への切り替え、3) Input System の構成要素の作成、が必要です。さらに先述の 3) Input System の構成要素として、a) Input Settings, b) Input Actions, c) Input Actions を使用するためのコンポーネント(Player Input)のゲームオブジェクトへの追加、があります。

Input System Asset のインストール

(1) Unity画面メニューから、Window > Package Manager を選択
(Package Manager画面が開きます)
(2) Package Manager のパッケージ一覧から、Input System を選択
(3) 画面右下の Install をクリック

f:id:tomo_mana:20200807001429p:plain
Input System Asset のインストール

※パッケージ一覧が表示されるまでに時間がかかることがあります。
※パッケージ一覧は アルファベット昇順(ABC...) に並んでいることに気が付くと探しやすくなるのですが、それでも量があるので検索窓を使った方が楽です。

Input Manager から Input System に切り替える

Input Manager から Input System に切り替えるには、プロジェクトの設定画面で変更します。
(1) Unity画面から、Edit > Project Settings... を選択
(Project Settings メニューが開きます)
(2) 左のメニューから、Player を選択
(3) 右のメニューから、パソコンマークのアイコンを選択
(4) Other Settingsを選択して展開します

f:id:tomo_mana:20200807000745p:plain
Project Settings > Player > OtherSettings

(5) メニューを下にスクロールしていき、Configuration 欄の Active Input Handlingを見つけます
(6) Active Input Handlingの入力欄をクリックします。
(7) Input System Package (New) を選択します。

f:id:tomo_mana:20200807001012p:plain
Active Input Handling

(8) 再起動をうながす警告が表示されますので、yes をクリックします。
(Unityが再起動します。再起動後は、再起動前と同じ Project Settingsの画面が表示されます。再起動後は、Input Manager の下に Input System Package が追加されます。)

Input Manager 側は無効になった警告が出るようになります。
Input System Package はこの時点では何も変更しません。

InputSettings(.inputsettings) を作成する

(1) Unity画面から、Edit > Project Settings... を選択
(Project Settings メニューが開きます)
(2) 左のメニューから、Input System Package を選択

f:id:tomo_mana:20200807145115p:plain
Input Settings 未作成

(3) 右のメニューから、Create settings asset をクリック
( Assets 下に、.inputsettings オブジェクトが生成され、Input System Package の非アクティブ部分がアクティブになります。)

f:id:tomo_mana:20200807145158p:plain
Input Settings 作成済
f:id:tomo_mana:20200807145243p:plain
Input Settings アイコン

Player Input コンポーネントを追加する

(1) 入力を受け付けたいコンポーネントで、
Add Component > Input > Player Input を選択
(Player Inputコンポーネントが追加されます)

InputActions(.inputactions) を作成する

先ほど追加したPlayer Inputコンポーネントで、
(1) Create Actions… をクリックする

f:id:tomo_mana:20200807145925p:plain
Player Input > Create Actions

(2) 名前を付けて保存します(拡張子.inputactions)
デフォルトでは 「(自分のプロジェクト名).inputactions」となっています。
(今回はデフォルトのままにしました)

以下は、生成された Input Actions のアイコンです。先ほど作成した Input Settings はあまり触ることはありませんが、こちらはキーを追加する時に触りますので、アイコンの違いで見分けます。

f:id:tomo_mana:20200807150015p:plain
Input Actions アイコン

※Input Actions は複数の作り方があるようですが、Player Input > Create Actions... で作成した時は、最初から PlayerUI の2つの Action Map が作成された状態の Input Actions が作成されるため、キー配置作業を大幅に短縮できます。

以下の図は、Input Actionsアイコンをダブルクリックしたときに開かれるキーを追加削除するウィンドウです(キーの追加は後述します)

f:id:tomo_mana:20200807150530p:plain
Input Actions > Action Map (Player)

※ゲームオブジェクトのコンポーネントにあるボタンから生成しているので、オブジェクト毎に作るような気がしてしまいますが、Input Actions は1プロジェクトに1つあれば事足りると思われます。

(以下の方法でも作成できますが、空の Input Actions が作られるため、キーを1から設定しなければならないので少し面倒です)
(1) Unity画面メニューから、Assets > Create > Input Actions を選択
(2) 確定する前に名前を付けてから保存します。

InputActions(.inputactions) を Player Input コンポーネントにアタッチする

入力を受け付けたいコンポーネントPlayer Input コンポーネントで、
(1) Actions 欄に、先ほど作成した InputActions(.inputactions) をドラッグ&ドロップ
(2) Actionsに欄が追加されるので、以下を設定する

  • Default Scheme : KeyboardMouse
  • Default Map : Player or UI (ここではPlayer)
f:id:tomo_mana:20200807151002p:plain
Player input に Input Actions をアタッチした状態

InputActions(.inputactions) を C#スクリプト(.cs) に出力する

(1) Projectウィンドウから、Input Actions を選択
(2) Inspectorウィンドウから、Generate C# Class にチェック
(以下の入力欄が表示されます)

  • C# Class File
  • C# Class Name
  • C# Class Namespace

デフォルトではプロジェクト名で生成します。
(今回はデフォルト名にしました)
(2) Apply をクリックします。
(C#スクリプトが作成されます)

※このスクリプトは生成するだけで、アタッチする必要はありません。ただ、キーを追加するたびにこの作業が必要になります(以前に作成した.csを上書きします。後述。)

Input Manager からの移植(C#スクリプトの修正)

Input Manager と Input System ではキーの受け取り方が異なるため、キーイベントを使用するC#スクリプトを修正(Input System方式に移植)する必要があります。

キー入力のテスト

第3回で実装したキー入力部を移植します。Input Manager との違いは、Input Systemでは Update() でのキー取得の他に、キーが押された瞬間の処理でキーを取得できることです。ここでは十字キーが押された時に発生する OnMove() イベントでキーを取得します。

コード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;    // ← 追加

public class PlayerControl2 : MonoBehaviour
{
    // キー入力
    private Vector2 input;
    
    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {    
    }
    
    // 新Input System (入力イベントで取得)
    void OnMove(InputValue value)
    {
        input = value.Get<Vector2>();
        Debug.Log(input);
    }
}

押された時と離された時のイベントをそれぞれ取得していることが分かります。

十字キーでキャラクターが動くテスト

第5回で実装した動作部を移植します。動作は以前のスクリプトと同じく OnFixedUpdate() に実装します。
コード

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

public class PlayerControl2 : MonoBehaviour
{
    // キー入力
    private Vector2 input;
    
    // 移動
    private float speed;
    private Rigidbody2D rigidBody;
    
    // Start is called before the first frame update
    void Start()
    {
        speed = 0.1f;
        rigidBody = GetComponent<Rigidbody2D>();
    }

    // Update is called once per frame
    void Update()
    {        
    }
    
    // 新Input System (入力イベントで取得)
    void OnMove(InputValue value)
    {
        input = value.Get<Vector2>();
    }
    
    void FixedUpdate() {
        // 移動
        if (input == Vector2.zero){
            return;
        }
        rigidBody.position += input * speed;
}

最後に取得したイベントがキーを離すイベントだった場合、入力が失われるため、少し動きが鈍くなった印象ですが、押した分しか移動しないので、キーを離した時のキャラクターが滑るような感触が無くなって操作しやすくなった気がします。(今後の改善が必要か)

アニメーション切り替えのテスト

第6回で実装したアニメーションの切り替え処理を移植します。
コード

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

public class PlayerControl2 : MonoBehaviour
{
    // キー入力
    private Vector2 input;
    
    // 移動
    private float speed;
    private Rigidbody2D rigidBody;
    
    // アニメーション
    private Vector2 scale;
    private Animator animator;
    
    // Start is called before the first frame update
    void Start()
    {
        speed = 0.1f;
        rigidBody = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {        
    }
    
    // 新Input System (入力イベントで取得)
    void OnMove(InputValue value)
    {
        input = value.Get<Vector2>();
    }
    
    void FixedUpdate() {
        // 移動
        if (input == Vector2.zero){
            return;
        }
        rigidBody.position += input * speed;
        
        // キャラクターの左右反転
        scale = this.transform.localScale;
        if( input.x > 0 ){
            scale.x = 1;
        } else if( input.x < 0 ){
            scale.x = -1;
        }
        this.transform.localScale = scale;
        
        // アニメーション方向決定
        animator.SetFloat("dirX", input.x);
        animator.SetFloat("dirY", input.y);
    }
}

移植時に発生したエラー

移植してテストすると、以下のエラーが発生します。主に Input Manager と Input System のイベント処理の違いによって発生していると思われます。対処方法も含めて、以下にまとめます。

InvalidOperationException(新旧方式の衝突)

EventSystem オブジェクトの Standalone Input Module が Input Manager 専用らしく、Input System にすると衝突してしまうようです。これは、Standalone Input Module を Input System 用のコンポーネントInput System UI Module に変更することで解決します。
(1) Hierarchyウィンドウから、EventSystem を選択します。
(2) Inspectorウィンドウから、Standalone Input Module を見つけます。

f:id:tomo_mana:20200807192519p:plain
EventSystem > Standalone Input Module

(3) Replace with InputSystemUIInputModule をクリックします。
(Standalone Input ModuleInput System UI Input Module に置換されます)

f:id:tomo_mana:20200807192654p:plain
EventSystem > Input System UI Module

また、Input Manager 方式のコードが存在すると、エラーが出ますので、Input Manager 方式のコードを無効化します。
たとえば、キャラクターにPlayerControl という C#スクリプトを割り付けていたとすると、
(1) Hierarchyウィンドウから、該当のオブジェクトを選択
(2) Inspectorウィンドウから、アタッチしたC#スクリプトを見つけます
(3) C#スクリプト名の左にあるチェックを外します

NullReferenceException (PlayerInputEditor.cs でエラー!?)

このエラーログをクリックすると、システムのC#スクリプトと思われる PlayerInputEditor.cs への参照になっています。Unityのバグ?と焦りますが、これは Input Actions の C#スクリプトへの出力を行っていない時に発生します。

Input System でのキーの追加

キーの追加

Input Actions を開く

(1) Projectウィンドウで Assetsフォルダを選択
(2) Input Actions(.inputactions) ファイルをダブルクリック

Input Actions の編集

UI側の Submit, Cancelアサインされていないので、ここに "Q" と "E" を割り当てる
何も割り当てが無いと イベント名[Any] と表示されている

f:id:tomo_mana:20200807211914p:plain
InputActions > UI (default)

Eの割り付け
(1) Action Maps : UI
(2) Actions : Submit
(3) Properties > Binding を選択
(4) PathSumbit▼ を選択
(5) 検索窓で E を押す > E [KeyBoard] を選択
(Listen をクリック > E を押す > E [KeyBoard] を選択 でも良い)
(6) Use in control schemeKeyboard&Mouse にチェック

f:id:tomo_mana:20200807212036p:plain
InputActions > UI (キー追加)

同じように "Q" を Cancel に登録します。

Input Actions の保存

Input Actions の保存は、Input Actions 画面内で Save Asset を押します。.inputactionsはファイルとして独立しているためか。

Input Actions を C#スクリプトに出力する

(1) Generate C# Class にチェックが入っていたら、チェックを外します。
(2) Edit の下にある Apply をクリック
(Generate C# Class の下にあるApply がアクティブになります)
(3) Generate C# Class にチェック
(4) Apply を選択
(.csファイルが更新される)
※キーを追加するたびにこの作業が必要です。

動作テスト

動作テスト以降は次回に挑戦します。

今回の作業に当たって、以下のサイトを参考にしました。
tsubakit1.hateblo.jp
gametukurikata.com

次回やること

ウィンドウの作成と動作確認(アイテムは仮で作ります)

(後記)
Input Systemの導入にかかるアクション数を考えると、Input Settings の生成と、Input Actions 保存時の C#スクリプトの自動生成は、開発環境側で自動化してくれてもいいと思います(ただの愚痴です)。

Unity学習#11 (Unity 2019.4.1f1) 操作の観察

今回から、ステータスとコマンドの表示に挑戦しようと思ったのですが、よく考えたらステータスとコマンドって、RPGの操作の肝というか、そう簡単にこれと一つに決められないなと今更ながら気づきました。そこで、今回は操作に関するデザインの観察とキー配置について考えました。参考にしたRPGのタイトルが古いですが、古いゲームの方が単純なインターフェースなので、まず古いゲームから考えていきます。最近のインタフェースにも興味があるので、そちらはそのうちに調べたいと思います。

スケーラビリティについてもざっくりと検討したかったので、キー配置について、ゲーム機(OS)間のインタフェースの違いを少しだけ掲載しています。androidiOSPlayStation系、Xbox系について触れています。

ステータスとコマンド

ステータス

そもそも論になりますが、RPGは「ロールプレイングゲーム」の略ですが、もともとは戦争をモチーフにした駆け引き(ボードゲーム)が起源と言われています。自分の殻を破る探索・冒険の先にある敵(障害となる相手)との駆け引き(衝突や葛藤)が描かれます。「ゲーム」そのものも、「試合(死合)」の意味で、やはり何か/誰かとの駆け引きになります。壮大な外の世界への探索・冒険だけでなく、心の内面への探索などにも同じモデルが適用されることがありますが、同じく駆け引きがあります。葛藤や摩擦があって、命の削り合いがあります。駆け引きに必要なものは「体力」「削る力」「削る速さ」の3要素です。

  • 体力(消費の対象)
  • 削る力(消費の単位)
  • 削る速さ(消費行動の優先度)

コマンド(端末への入力)

RPGでは、冒険・探索の中や自分の外にある何か/誰かとの接触があります。その一部は謎解きを含みます。謎解きには、「調べる」が生じます。また、調べるには、必ず「中断」があります。あらゆるコマンド(命令、指示)は、進む/深める要素と戻る要素があると考えると、2つの異なる入力が必要になります。それと移動手段=十字キーが必要です。

  • 調べるボタン
  • 中断するボタン
  • 選択するボタン(十字キー

コマンド(ゲームへの指示)

ゲームへの指示は、「自分の状態を知る」ことと、「対象に何かをする」があります。「持ち物を使って」物事を解決することもあります。この持ち物は時代を超えて、「アイテム」や「武器」「呪文」など、様々な要素があります。しかし「使う」ことは共通しています。「装備する」は「使う」ものを毎回選択することを省略しています。

  • 状態を知る
  • 持っているものを選ぶ
  • 持っているものを使う

ステータス表示のタイミング

ステータス表示のタイミングは、少なくとも3つあります。「常時」「ボタンを押した時」「待っている時」です。「待つ」のは、時間を意識させない初期のRPGにおいて、狭い画面をできるだけ広く使うために工夫したものです。

  • 常時
  • ボタンを押す
  • 待つ

過去のタイトルの比較

ここからは、過去のタイトルについて比較します。過去のタイトルが、「常時」「ボタンを押した時」「待っている時」に、どのように「状態」や「コマンド」を表示したかを比較します。比較すると、各タイトルの情報の仕方だけでなく、ゲームプレイヤーを飽きさせない工夫が随所に隠れていることに気づかされます。ユーザーインタフェースは、ベストプラクティスはあっても、絶対解のない世界かもしれません。実装だけでなく、画面の向こう側にいるプレイヤーとの対話の形を模索する、製品開発の本来の目的に、開発者を立ち戻らせてくれます。

RPG

ここでは、昔のターン制のゲームを考えます。Nethack/ローグは、知らない人も多いですが、「トルネコの不思議なダンジョン」などの基になったゲームと言われています。

ゲームタイトル 常時 待ち ボタン押下
ドラゴンクエスト (初代:1986) -- 状態 コマンド
ファイナルファンタジー (初代:1987) -- -- コマンド+状態
Nethack (ローグ系:1987) 状態 ヒント コマンド

アクションRPG

ここでは、昔のリアルタイムバトルのゲームを考えます。マインクラフトは最近ですね。

タイトル 常時 待ち ボタン押下
ゼルダの伝説 (初代:1986) 状態 アイテム選択
聖剣伝説2 (初代:1993) 状態 コマンド(円形)
マインクラフト (2011~) 状態 コマンド+アイテム

敵が止まってくれないアクションRPGは「常時」状態を表示するのに対し、自分が動かなければ敵も動かない初期のRPGは、ゲームプレイヤーが「待つ」というタイミングを非常に重視したと考えられます。

PCゲームでのキー配置

先ほどの話から、初期のファミコン十字キー+AB2つのボタンでできていたのが本当に洗練されたミニマルなデザインと感じました。最近は6~8ボタン、PCゲームになると特定のアイテムやキー操作のショートカットまで含めると、キーボードの1/3~1/4は使用しているようです。各メーカーのキー配置はそれぞれ異なりますが、PCゲームについていうと、少なくとも「WASD」か「ESDF」に分かれるようです。それぞれの名前は十字キーに使用する4つのボタンを指しており、これは右手でマウス(カーソル+クリック)、左手で補助的にキーボードを使うことを想定しているようです。

WASD

f:id:tomo_mana:20200731070604p:plain
WASD

ESDF

f:id:tomo_mana:20200731070622p:plain
ESDF

昔の両手式PCゲーム(記憶)

f:id:tomo_mana:20200731073722p:plain

媒体ごとのキーの取得

以下は汎用的なキー配置の検討のためにざっくり調べたもので、自分で確かめていない部分が多いです。しかしながら、複数のゲーム機に対応したい時に、どんな操作感を検討したらを検討するのに役立つと思い、以下にまとめました。

PC

Unityでは、PCで操作/デバッグするためのデフォルト設定があります。これはInput Managerと呼ばれるもので設定します(最近はInput Managerの後継的な仕組みがあり、次回とり上げます)。UnityではWASDがデフォルトです。また、以下の通り、十字キー、Ctrlなど文字以外のキーが積極的に使われており、文字キーに対しては、自由に割り当てられる状態であることが分かります。また、マウスクリックにCtrl、Alt、Shiftと同じ機能を持たせていることが分かります。(どのキーにどんな機能を持たせるかは、先ほどのInput Managerで変えられます)

f:id:tomo_mana:20200801001841p:plain
Unityデフォルト(Keyboard)
f:id:tomo_mana:20200801233235p:plain
Unityデフォルト(Mouse)

Unityでは、キーイベントを2種類で定義しています。どの端子が押されたかをキーコード(KeyCode)と呼び、その端子がどういう意味で通知されるかをバーチャル軸(Axis)またはボタン(Button)と呼んでいます。

入力端子 キーイベント名 バーチャル軸名/ボタン名
インタフェース Input.GetKeyDown(key/name) Input.GetButtonDown(buttonName)
引数 (KeyCode/String) (String)
KeyCode.LeftArrow "Horizontal"
KeyCode.RightArrow "Horizontal"
KeyCode.UpArrow "Vertical"
KeyCode.DownArrow "Vertical"
W KeyCode.W "Vertical"
A KeyCode.A "Horizontal"
S KeyCode.S "Horizontal"
D KeyCode.D "Vertical"
Ctrl KeyCode.LeftControl "Fire1"
Alt KeyCode.LeftAlt "Fire2"
Shift KeyCode.LeftShift "Fire2"
Space KeyCode.Space "Jump" & "Submit"
Enter KeyCode.Return "Submit"
Esc KeyCode.Escape "Cancel"
左クリック KeyCode.Mouse0 "Fire1"
右クリック KeyCode.Mouse1 "Fire2"
中央ホイールクリック KeyCode.Mouse2 "Fire3"

キーの入力は、パソコンキーボードにあるボタンの他に、特定のメーカーのコントローラにしかついていない入力素子(JoyStick/3rd Axis以降)があります。どのキーがどのイベントで通知されるかはWindowsなど開発に使用するOS、各ゲーム機用のコントローラの仕様の両方で規定されています。各端末のキー配置は、実際にゲーム機用の端末を接続して確認することになります。しかし、先述のInput Managerを使うことで、複数のゲーム機で共通のコードを組むことが可能です。今後、Unityがバージョンアップを重ねることで、各ゲーム機の入力はもっと調べやすくなるのではと期待します。
先ほどのInput Managerと関係して、Unityでは、複数のゲーム機に対応するために、物理的なキーの名前(KeyCode)よりも、どんな意味合いを持ったキー(Axis/Button)が押されたか、という視点で作り込むことが奨励されています。後者の場合、押されたかどうかの確認にはGetButtonDown()を使用し、押された量の確認にはGetAxis()を使用します。GetButtonDown()に使われているのがボタン名で、GetAxis()に使われているのがバーチャル軸名ですが、どちらもInput Managerでは"name"という名前で定義されています。

(参考:Unityリファレンス)
KeyCode - Unity スクリプトリファレンス

この記事に一通りまとめた後で、Input Manager が新しくなったことを知りました。次回のキー配置を考える時に、一緒に確認したいと思います。
tsubakit1.hateblo.jp

iPhone

iPhoneは、基本的に画面のタッチイベントを使用します。
一つだけあるボタン(ホームボタン)について、有力な情報が得られていません。KeyCode.Homeでしょうか?

後述するandroidと共通ですが、タッチイベントについては、入力端末が無い場合、指1本の動作に限って、マウスで代用ができるようです。

Input.touchCount : 触れている指の数
Input.touches[] : それぞれの指が触れている場所(1本指の場合、touches[0])
.position : 押した瞬間、離した瞬間の場所を記憶します。
.mousePosition : 押し付けている間、現在の場所を記憶します。
TouchPhase.Begin, Moved, Ended : 押し始めか、押し中か、離した瞬間か

deve-cat.com

android

android端末も、iPhoneと同じく基本的には画面のタッチイベントを使用します。
物理的な端子は2つ、または3つのキーがあります(全くないものもあります)。現物が無いので、既存のサイトの記事を参考にするしかありませんが、KeyCode.Escape, KeyCode.Home, KeyCode.Menu と思われます。(キー配置は端末によって異なるようです)

matudozer.blog.fc2.com

以下はゲーム機毎のキーマップをまとめていただいている方がいらっしゃるので、紹介だけです。かっこ内の数字は、備忘のためいつ頃発売されたゲーム機かを示しています。

PS Vita (2011-)

qiita.com

Xbox One (2013-)

hakonebox.hatenablog.com

尚、今回のキーボードとマウスのイラストは以下のサイトからお借りしました。ありがとうございます。
キーボードのイラスト・無料イラスト素材倉庫/人体図イラスト・フリーダウンロード

次回やること

キー配置の決定、ステータス、コマンド画面設計

Unity学習#10 (Unity 2019.4.1f1) フェードイン・アウト#2~ワープ処理との結合

画面のフェードイン・アウトが思ったよりコード量があるので、前回と今回の2回の記事に分けました。今回は第9回で作ったフェードイン・アウト処理と第8回のワープ処理をくっつけます。

フェードイン・アウトのインタフェース(第9回)

前回のフェードイン・アウト処理を参照するためには、以下を追加します。

コード

private FadeManager fader;

// 処理
if ( fader.IsFading() == false ){
    fader.FadeIn();
}

ただし、フェードイン・アウトの処理を行う間、ワープ処理は待つ必要があるので、ワープ処理内に状態を持たせる必要があります。

ワープの状態

状態遷移図

キャラクターがワープポイントに衝突した時、ワープポイントはフェードアウトを待ってワープします。ワープした後、フェードインを待ってワープポイントは再びキャラクターが衝突してくるのを待ちます。
これを状態遷移図にすると以下になります。

1) 衝突待ち
↓ 衝突
2) フェードアウト
↓ 完了
3) ワープ
↓ 完了※
4) フェードイン
↓ 完了
1) 衝突待ち
※実際は、3) ワープ処理は、同一平面での座標移動の場合は「待ち」が発生しないので、今回は状態でなく処理の一部にします。今後、マップの読み込みなどで時間がかかる場合は「待ち」にする可能性があるかもしれません。

状態遷移表

イベントと状態を整理して、状態遷移表を作ります。

f:id:tomo_mana:20200728083203p:plain
ワープの状態遷移表

ワープの修正

状態定義

状態を列挙体で定義します。
コード

    enum WarpState {
        Idle,
        Triggered,
        FadeOut,
        FadeIn
    }
    private WarpState state;

イベント処理の追加

今回は、イベントが「衝突」以外はすべて「フェード処理完了」になるので、Update() 関数の中で一つのif文にまとめています。

コード

public class Warp : MonoBehaviour
{
    // ワープ中の状態
    enum WarpState {
        Idle,
        Triggered,
        FadeOut,
        FadeIn
    }
    private WarpState state;
    
    // Start is called before the first frame update
    void Start()
    {
        state = WarpState.Idle;
    }
    
    // Update is called once per frame
    void Update()
    {
        if( state != WarpState.Idle ){
            if( fader.IsFading() == false ){
                // 次の処理へ
                switch( state ){
                    case WarpState.Triggered:
                        state = WarpState.FadeOut;
                        break;
                    case WarpState.FadeOut:
                        state = WarpState.FadeIn;
                        break;
                    case WarpState.FadeIn:
                        state = WarpState.Idle;
                        break;
                    default:
                        break;
                }
            }
        }
    }
    
    void OnTriggerEnter2D (Collider2D other) {
        if( state == WarpState.Idle )
        {
            state = WarpState.Triggered;
        }
    }
}

衝突時の処理からワープ処理をくくりだす

isTriggerで使うことができたCollider2Dを直接使えなくなる(参照が変わる)ため、ワープポイントにぶつかったキャラクターCollider2Dをワープポイントは覚えておかなければなりません。

コード(修正前)

public class Warp : MonoBehaviour
{
    private Vector3 target;  // ← ワーク
    
    void OnTriggerEnter2D (Collider2D other) {
        target = other.gameObject.transform.position;
        target.x = warpPoint.x;
        target.y = warpPoint.y;
        other.gameObject.transform.position = target;
    }
}

コード(修正後)

public class Warp : MonoBehaviour
{
    private Collider2D target;  // ← ワープポイントに衝突したCollider(キャラクター)を保持
    private Vector3 v;  // ワーク

    // ワープ処理    
    void WarpTarget()
    {
        v = target.gameObject.transform.position;
        v.x = warpPoint.x;
        v.y = warpPoint.y;
        target.gameObject.transform.position = v;
    }
    
    void OnTriggerEnter2D (Collider2D other) {
        target = other;
        WarpTarget();
    }
}

状態遷移処理の実装

下準備ができたので、いよいよ状態・イベントにフェードイン・アウトとワープ処理を追加します。

コード

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

public class Warp : MonoBehaviour
{
    public Vector2 warpPoint;
    private Collider2D target;  // ← ワープポイントに衝突したCollider(キャラクター)を保持
    private Vector3 v;
    
    private FadeManager fader;
    
    // ワープ中の状態
    enum WarpState {
        Idle,
        Triggered,
        FadeOut,
        FadeIn
    }
    private WarpState state;
    
    // Start is called before the first frame update
    void Start()
    {
        state = WarpState.Idle;
    }
    
    // ワープ処理
    void WarpTarget()
    {
        v = target.gameObject.transform.position;
        v.x = warpPoint.x;
        v.y = warpPoint.y;
        target.gameObject.transform.position = v;
    }
    
    // Update is called once per frame
    void Update()
    {
        if( state != WarpState.Idle ){
            if( fader.IsFading() == false ){
                // 次の処理へ
                switch( state ){
                    case WarpState.Triggered:
                        fader.FadeOut();
                        state = WarpState.FadeOut;
                        break;
                    case WarpState.FadeOut:
                        WarpTarget();
                        fader.FadeIn();
                        state = WarpState.FadeIn;
                        break;
                    case WarpState.FadeIn:
                        state = WarpState.Idle;
                        break;
                    default:
                        break;
                }
            }
        }
    }
    
    void OnTriggerEnter2D (Collider2D other) {
        if( state == WarpState.Idle )
        {
            target = other;
            state = WarpState.Triggered;
        }
    }
}

ワープ時に、フェードアウト・インがかかるようになり、いい雰囲気になりました。

エフェクトの着脱

ワープのエフェクトを元に戻すことはさほど大変ではない(コードを戻すだけ)ですが、今後、特定のワープに他のエフェクトを付けたい場合のため、FadeManagerをプログラムの外に出して、付け替えができるようにします。
コード

public class Warp : MonoBehaviour
{
    [SerializeField] FadeManager fader;

    void OnTriggerEnter2D (Collider2D other) {
        if( state == WarpState.Idle )
        {
            target = other;
            if( fader == null ){
                WarpTarget();
            } else {
                state = WarpState.Triggered;
            }
        }
    }
}

※実際に他のエフェクトを作った場合は、FadeManager は Interface にしないといけないと思いますが、そのうちに考えます。

これからやること

なんかそれっぽくなってきて、心に火がついてきました。ここからはどの順番でやるか選択していく必要があります。まず面白さの追求より基本のエンジンを観察していくので、以下のようにします。(面白さ=ゲームバランスは後から追求します)

(1) 基本:

  • ステータス画面(HP、速度だけ)
  • 戦闘画面

 絶対勝つ戦闘(クリアのデバッグ
 絶対負ける戦闘(ゲームオーバーのデバッグ
 ゲームオーバーの定義

  • キャラクターエンカウント(当たり判定の利用)
  • ボスエンカウント(当たり判定→戦闘→フラグ設定)
  • マルチシーン(タイトル、ゲーム、エンディング)

(2) シナリオの発生:

  • イベントフラグ(ダンジョンクエストの解放、ボス戦の解放)

(3) ゲームバランス(駆け引き)の追求:

  • ランダムエンカウント
  • 会話の発生(当たり判定の利用)
  • アイテムの追加―アイテム屋(回復、速度UP、速度DOWN、攻撃UP、攻撃DOWN、効果をはじく)
  • 武器の追加(攻撃UP+アイテム効果付与)
  • 貨幣の追加

※今回はマップをシングルシーンで作成しましたが、マルチシーンで作成する方法は全部作ってから挑戦します。

  • ランダムダメージ

 ランダムダメージ
 会心の一撃、痛恨の一撃

(4) 長編化

  • マルチシーン(シーン間のマップ移動、シーン間のデータ共有)
  • ロード・セーブ

Unity学習#9 (Unity 2019.4.1f1) フェードイン・アウト#1

今回はフェードイン・アウトに挑戦します。(どっちか分からなくなるので、フェードインは「画面が明るくなる」、フェードアウトは「画面が暗くなる」です)

画面を明るくする、暗くする処理と、それをワープのエフェクトに使うのと両方をする予定でしたが、後者のボリュームが大きいので2回に分けます。今回は、フェードイン・アウトの処理だけ作っておき、ゲーム開始時(Start関数)に動作を確認します。
フェードイン・アウトは、画面の手前に全体を覆う黒いパネルを用意して、透明度を変えることで表現する方法がよく使われているようです。このパネルとして、Canvas.Image を使います。Canvas は、ゲームの操作画面などを配置するもので、ステータス表示や会話、プレイヤーを操作するボタン類などに使われます。キャラクターやカメラがどこにいても、常に画面の同じ位置に表示できます。

今回はコードがほとんどになりました。

Canvas.Imageの追加

Hierarchyウィンドウで右クリック > UI > Image を選択

急にいくつかのオブジェクトが追加されますが、今は気にしないことにします。

f:id:tomo_mana:20200727195942p:plain
Canvas.Image

Canvas.ImageへのC#スクリプトの追加

スクリプトの追加は過去の記事でまとめたため、手順は省略します。
今回はフェードイン・アウトを管理するので、FadeManager という名前にします。

Canvas.Imageの色・透明度の変更

画面全体を覆うパネルが用意されました。これを黒く塗りつぶします。

(1) Hierarchyウィンドウで Canvas > Image を選択
(2) InspectorウィンドウでImage > Colorの色がついているゲージを選択
(3) Colorウィンドウが表示されるので、R, G, B, A に以下を入力します。

パラメータ 数値
R 0
G 0
B 0
A 255

※Aはアルファ値(透明度)

Canvas.Imageの大きさの変更

Imageの大きさの単位

Imageの大きさはRectTransformのWidth、Heightです。RectTransformはこれまでキャラクターやTilemapで扱ってきた Transform とは異なり、1 = 1dotです。(キャラクタやTilemapは1マス単位)

Imageの大きさを取得する

ImageのRectTransformのサイズは Vector2で取得できます。Vector2は RectTransform.sizeDelta から取得します。Vector2はキャラクターの移動時にも使用しましたが、要素は x と y で、キャラクターの移動時は「方向」と「移動量」を表していました。RectTransform.sizeDeltaでは、x, y は「四角形のサイズ」(Width, Height) を表しています。
コード

public class FadeManager : MonoBehaviour
{
    void Start()
    {
        RectTransform rectTransform = GetComponent<RectTransform>();
        Vector2 v = rectTransform.sizeDelta;

        Debug.Log("RectTransform.width = " + v.x);  // Width を表す
        Debug.Log("RectTransform.height = " + v.y);  // Height を表す
    }
}

画面の大きさを取得する

画面の解像度によってImageをリサイズするため、コードから画面サイズを取得します。
コードで大きさを取得する時に使うのはScreen.WidthとScreen.Heightです。このScreenの単位はImageと同じ単位(1 = 1dot)です。

コード

public class FadeManager : MonoBehaviour
{
    void Start()
    {
        Debug.Log("Screen.width = " + Screen.width);
        Debug.Log("Screen.height = " + Screen.height);
    }
}

前回と同様に、各画面を設定した時にどれくらいの大きさになるかを以下に示します。

解像度 X (Width) Y (Height)
16:9 424 238
16:10 386 241
5:4 301 241
4:3 321 241
3:2 362 241
960x600 960 600
standalone(1024x768) 1024 768

Image を画像の大きさにする

Vector2 RectTransform.sizeDelta も、十字キーの動きをキャラクターに反映する時と同じで、直接書き換えることができません。以下のように実装します。
コード

public class FadeManager : MonoBehaviour
{
    private float canvasMargin = 1.1f;

    void Start()
    {
        RectTransform canvas = GetComponent<RectTransform>();
        Vector2 v = canvas.sizeDelta;
        v.x = Screen.width * canvasMargin;
        v.y = Screen.height * canvasMargin;
        canvas.sizeDelta = v;
    }
}

透明度(アルファ値)の変更

フェードイン・アウトは、透明度(アルファ値)を変更します。
アルファ値は Color.a で取得します。Canvas.ImageのColorは、Imageの直下にいるので、Image.Colorで取得できます。

ピッカーでは0(透明)~255(不透明)ですが、
Color.a では0.0f(透明)~1.0f(不透明)です。

透明度を取得する

コード

public class FadeManager : MonoBehaviour
{
    void Start()
    {
        Debug.Log("alpha = " + GetComponent<Image>().color.a);
    }
}

透明度の変更

アルファ値の変更も、Vector2 と同じで、直接変更ができません。
そのため、以下のようにします。
コード

public class FadeManager : MonoBehaviour
{
    private Image image;
    private Color color;

    void Start()
    {
        image = GetComponent<Image>();    // imageへの参照を取得する
        color = image.color;    // image.color の各要素が自分の作業用のcolorにコピーされる
        color.a = 0.5f;   // 自分の作業用のcolorの値を入れ替える
        image.color = this.color;    // imageの参照先に自分のcolorをコピーする
    }
}

フェードイン

フェードイン処理については、以下のサイトを参考にしました。
kenko-san.com

●外からフェードイン・アウトを指定できるようにする
●今フェードイン・アウト中かをわかるようにする
●フェードイン・アウトの時間をオブジェクト毎に変更できるようにする
(1つのコードで複数のフェードイン・アウトを作れるようにする)
という仕様にしようと思います。

それぞれの実装方法についてまとめます。

フェードインの処理

コード

public class FadeManager : MonoBehaviour
{
    private Image image;
    private Color color;
    private float fadeTime = 1.0f;

    void Start()
    {
        image = GetComponent<Image>();
        color = image.color;
        isFadeIn = true;    // ←起動時にフェードイン(真っ暗→明るく)
    }
    void Update()
    {
        if (isFadeIn){
            color.a -= Time.deltaTime / fadeTime;
            if (color.a <= 0.0f){
                isFadeIn = false;
                color.a = 0.0f;
            }
            image.color = this.color;
        }
    }
}

フェードイン中かどうか

コード

public class FadeManager : MonoBehaviour
{
    private bool isFadeIn;

    public bool IsFading()
    {
        bool r = false;
        
        if( isFadeIn == true )
        {
            r = true;
        }
        return r;
    }
}

フェードインにかかる時間を画面から指定する。

第x回に書いた、public または [SerializeField] を使うことで、フェードインにかかる時間を外から入力できます。設定が無い時はフェードイン・アウトが一瞬で完了するようにします。

コード

public class FadeManager : MonoBehaviour
{
    [SerializeField] float fadeTime;

    // Start is called before the first frame update
    void Start()
    {
        if( fadeTime == 0 ){
            fadeTime = Time.deltaTime;
        }
    }
}

フェードイン関連の全処理

コード

public class FadeManager : MonoBehaviour
{
    private Image image;
    private Color color;
    [SerializeField] float fadeTime;
    
    private bool isFadeIn;
    
    
    void Start()
    {
        // 透明度変更の準備
        image = GetComponent<Image>();
        color = image.color;
        isFadeIn = true;    // ←起動時にフェードイン(真っ暗→明るく)
        
        // フェードイン・アウト速度の初期化
        if( fadeTime == 0 ){
            fadeTime = Time.deltaTime;
        }
    }
    
    // フェードイン・アウト中か確認
    public bool IsFading()
    {
        bool r = false;
        
        if( isFadeIn == true )
        {
            r = true;
        }
        return r;
    }
    
    void Update()
    {
        if (isFadeIn){
            color.a -= Time.deltaTime / fadeTime;
            if (color.a <= 0.0f){
                isFadeIn = false;
                color.a = 0.0f;
            }
            image.color = this.color;
        }
    }
}

フェードアウト

フェードアウトの追加

これまでに調べたことを使って、フェードインと対称的な処理を追加したら、フェードアウト処理も追加できます。
コード

public class FadeManager : MonoBehaviour
{
    private Image image;
    private Color color;
    [SerializeField] float fadeTime;
    
    private bool isFadeIn, isFadeOut;
    
    void Start()
    {
        // 透明度変更の準備
        image = GetComponent<Image>();
        color = image.color;
        isFadeIn  = false;    // ←起動時にフェードイン(真っ暗→明るく)
        isFadeOut = false;    // ←起動時にフェードアウト(明るい→真っ暗)
        
        // フェードイン・アウト速度の初期化
        if( fadeTime == 0 ){
            fadeTime = Time.deltaTime;
        }
    }
    
    public void FadeIn()
    {
        if ( !IsFading() ){
            isFadeIn = true;
        }
    }
    
    public void FadeOut()
    {
        if ( !IsFading() ){
            isFadeOut = true;
        }
    }
    
    // フェードイン・アウト中か確認
    public bool IsFading()
    {
        bool r = false;
        
        if( (isFadeIn == true) || (isFadeOut == true)  )
        {
            r = true;
        }
        return r;
    }
    
    void Update()
    {
        if (isFadeIn){
            color.a -= Time.deltaTime / fadeTime;
            if (color.a <= 0.0f){
                isFadeIn = false;
                color.a = 0.0f;
            }
            image.color = this.color;
        } else
        if (isFadeOut){
            color.a += Time.deltaTime / fadeTime;
            if (color.a >= 1.0f){
                isFadeOut = false;
                color.a = 1.0f;
            }
            image.color = this.color;
        }
    }
}

フェードイン・アウトを外から指定する(インタフェースの確認)

フェードイン・アウトには時間がかかるので、ワープ側でフェードイン・アウトしているのか、またフェードイン・アウトの指示を出せるようにします。
コード

// 他のファイルからの呼び出し
private FadeManager fader;

// 処理
if ( fader.IsFading() == false ){
    fader.FadeIn();
}

名前空間を設定していないので、他のファイルからFadeManagerという文字(あるいは自分が付けた名前)を呼び出せば良い(名前空間を設定していないと、同じ名前が使えないので注意が必要)

次回、上記のインターフェースを使ってワープ中のフェードイン・アウトを追加します。

次回やること

ワープ時にフェードイン・アウト効果を追加する

Unity学習#8 (Unity 2019.4.1f1) キャラクターのワープ

第8回は、キャラクターのワープに挑戦しました。
ここまで実装を続けてくることで、Unityの基本的な構想が分かってきて、キャラクターのワープの実装についても勘が働くようになってきました。
キャラクターのワープは、第5回の当たり判定と、前回(第7回)の中で触れたパラメータを画面上から入力できるようにする処理の組み合わせで実現します。また、データ容量を軽くするため、Create Emptyを使用します。

移動を考慮したマップの構成

第7回で、フィールドと町、ダンジョンとの間の移動は、とりあえず実装ハードルの低い同一平面上の移動で実現することにしました。マップを同一平面に作る場合は、マップ間の移り込みを避ける必要があります。

マップの間に必要な隙間

iPhoneandroid、その他の媒体での公開を検討する時に、媒体ごとに画面のピクセル数や縦横比が変わりますので、媒体ごとの差を埋めるための作り方は今後調べていくとして、どんなピクセル数、縦横比でも、マップとマップを何マス開けるかは事前に調べておく必要があります。
Unityのデフォルトで設定できる縦横比については、マップの端から以下のマス数(左右, 上下)を空ける必要があります。(Main CameraのSizeを7にした場合)

f:id:tomo_mana:20200724135408p:plain
縦横比と必要なマップ間隔の確認
縦横比 拡大率(Scale) 左右マス数(X) 上下マス数(Y)
16:9 1 13 7
16:10 1 11.5 7
3:2 1 11 7
4:3 1 10 7
5:4 1 9.2 7
Standalone(1024x768) 0.421 10 7
960x600 0.538 11.7 7

ワープポイントの作成

Emptyオブジェクトの作成

ワープポイントは、Emptyオブジェクトを使用します。作成後、座標はリセットしておきます。

(1) Hierarchyウィンドウの空きスペースで右クリック
(2) Create Empty を選択
(3) Inspectorウィンドウで名前を変更(WarpPoint)
(4) Inspector > Transform欄の縦3個の点が並んでいるアイコンを右クリック
(5) Reset を選択

Emptyオブジェクトの作成(各ワープポイント)

Emptyオブジェクトは位置情報など最低限の情報だけを持っているオブジェクトで、複数のオブジェクトを束ねるのに適しています。ワープポイントは複数作成するので、上記で作成したオブジェクトの下に、すべてのワープポイントを束ねると良いかと思います。

(1) Hierarchyウィンドウで先ほど作成したEmptyオブジェクトを選択
(2) オブジェクトの名前の上で右クリック > Create Empty を選択
(3) Inspectorウィンドウで名前を変更

ワープポイントの名前

名前は、どこからどこへ、が分かるように付けるようにするため、以下のようにします。
フィールド(Field:F)、町(Village:V)、ダンジョン(Dangeon:D)
フィールド→町:F2V

ワープポイントに当たり判定を付加する

ワープポイントにBox Colliderを付加します。
パラメータはisTriggerにチェックを付ける以外は第5回の障害物と同じです。

ワープポイントに質感を付加する

ワープポイントにRigidBody 2Dを付加します。
パラメータは第5回と同じく、BodyTypeをstaticにします。

f:id:tomo_mana:20200724120521p:plain
当たり判定

ワープC#スクリプトの作成

ワープ先を画面で管理できるように、また複数のワープポイントに同じスクリプトを適用できるように、ワープ先(warpPoint)をpublic変数として定義します。

(1) Projectウィンドウで右クリック
(2) Create > C# Script を選択
(3) 確定する前に、オブジェクト名を入力します
(ここでは、Warpとします)
(4) Projectウィンドウの空きスペースをクリックして、名前を確定します

(5) Projectウィンドウ上で右クリック > Openでコードを開きます。
(6) コードは以下のようにしました。

コード

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

public class Warp : MonoBehaviour
{
	public Vector2 warpPoint;
	private Vector3 target;
	
    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }
    
    void OnTriggerEnter2D (Collider2D other) {
    	target = other.gameObject.transform.position;
    	target.x = warpPoint.x;
    	target.y = warpPoint.y;
    	other.gameObject.transform.position = target;
    }
}

ワープC#スクリプトのアタッチ

先ほどのコードをワープポイントオブジェクトにアタッチします。
(1) Hierarchyウィンドウでコードを付与するオブジェクトを選択
(2) Projectウィンドウでワープポイントのコードを選択
(3) Inspectorウィンドウの空いているスペースにドラッグ&ドロップ

ワープポイントに移動元、移動先を入力する

(1) Hierarchyウィンドウでコードを付与するオブジェクトを選択
(2) InspectorウィンドウのTransform > Positionに移動元の座標を入力
(3) InspectorウィンドウのC# Script欄に「Warp Point」が追加されます。
移動先の座標を入力します。

f:id:tomo_mana:20200724120946p:plain
移動元、移動先

ここまでで、意図したとおりにワープしてくれるかテストします。

ワープポイントの設計

往路と復路

ワープポイントは、一方通行でない限り、行きと帰りがあります。往路と復路は別々に用意します。

往路と復路で座標をずらす

移動元と移動先の座標は、重ならないようにします。(重なってしまうと、移動した瞬間に当たり判定が走ってしまい、無限に行ったり来たりを繰り返してしまうため)

ラベル Object名 移動元(X,Y) 移動先(X,Y)
フィールド→町 WarpPoint>F2V (0,-2) (25, 0)
町→フィールド WarpPoint>V2F (0, 24) (0, -3)
f:id:tomo_mana:20200724120136p:plain
町とフィールドの移動

座標の設定

先述の画面移りを防止するため、フィールド、町、ダンジョンの端と端を20マス空けるとして、座標を以下のようにします。

f:id:tomo_mana:20200724115536p:plain
ワープポイント
ラベル Object名 移動元(X,Y) 移動先(X,Y)
フィールド→町 WarpPoint>F2V (0,-2) (25, 0)
町→フィールド WarpPoint>V2F (0, 24) (0, -3)
フィールド→ダンジョン WarpPoint>F2D (0, 2) (-25, 0)
ダンジョン→フィールド WarpPoint>D2F (0, -24) ((0, -1)

Tilemapとのずれを補正する

第5回に書いた通り、座標系とTilemap座標には半マスずれています。第5回ではキャラクターをずらしましたが、ワープポイントを使いやすくするためにTilemap側を半マスずらすように修正します。

f:id:tomo_mana:20200724115813p:plain
Tilemapの座標(Grid)

キャラクターの座標は元の(0, 0)に戻します。

f:id:tomo_mana:20200724115851p:plain
Playerの座標 (0, 0に戻す)

ワープポイント(2点目以降)の作成

作り方は1点目と同じです。
(1) ワープポイントを束ねるオブジェクトの下に、Emptyオブジェクトを作成
 名前の付け方は移動元→移動先
 パラメータはリセット
 Transform > Position に移動元座標を入力
(2) 作成したEmptyオブジェクトに当たり判定(Box Collider)を付加
 isTrigger にチェック
(3) 作成したEmptyオブジェクトに質感(RigidBody 2D)を付加
 BodyType = static
(4) 先ほど作成したC#スクリプト(Warp)を付加
(5) C#スクリプト > Warp Point にワープ先座標を入力

次回やること
画面のフェードアウト・フェードイン

Unity学習#7 (Unity 2019.4.1f1) マップの再描画~カメラワーク

最低限のマップができ、キャラクターが四方を向くようになったので、いよいよ町やダンジョンへのワープを実装していこうと思いましたが、先にキャラクターに合わせてマップの再描画(古い表現ですね)が必要なことに気が付きました。
町やダンジョンへのワープは、目的地と衝突→画面フェードアウト→キャラクター移動→画面フェードイン、となると考えています。目指すのは最小形のRPGですが、キャラクター移動が「同一平面」か「複数の平面」か想像できていませんでした。まず敷居が低そうな同一平面で作る方向ですが、同一平面上にワールド、町、ダンジョンをスクロール無しで表示できるほど小さく作っても、ワープする時に画面の再描画が必要になります。
Unityでは、キャラクターに合わせてマップがスクロールする描画を、カメラワークで表現しています。これは3Dの機能に2Dでも使えるようにしたUnityの非常に優れた設計で、カメラをキャラクターに追従させることで、キャラクターが動く時に、マップが追従するのを実現します。

カメラの追従

毎回ですが、分かってしまえば大した苦労もなくカメラを追従できるようになります。ただ分かるまでがハマりました。

C#スクリプトを作成する

スクリプトをオブジェクトに割り当てる方法は第2回と同じです。

1) Projectウィンドウで右クリック > Create > C# Script をクリック
(ProjectウィンドウにC#スクリプトのアイコンが生成される。ファイル名を入力できる状態になっている)
2) ファイル名を入力してから確定
※注意:ファイル名を最初に確定した時に、中のファイルが生成される仕組みで、最初に付けたファイルの名前がクラス名になります。最初に確定する前にファイル名を入力します。
(ここではCameraControllerと名前を付けました)

C#スクリプトを記述する

次に、カメラ用のスクリプトを記述します。
カメラも、キャラクターと同じくXYZ座標を持っていますので、カメラのXY座標をキャラクターのXY座標に合わせる処理を追加します。この時に大事なのは、カメラはキャラクターよりも手前(Z座標ではマイナス方向)にいる必要があります。そのため、Z座標だけは、キャラクターのZ座標を使わないのがポイントになります。ここでは、ゲームの開始時(Start)に、カメラ自身のZ座標だけを取り出して、画面が描画されるたび(Update)に使いまわしています。thisは自分(ここではカメラ)、targetは被写体です。

コード

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

public class CameraController : MonoBehaviour
{
    public GameObject target;
    private Vector3 pos;
    
    // Start is called before the first frame update
    void Start()
    {
        pos.z = this.transform.position.z;
    }
    
    // Update is called once per frame
    void Update()
    {
        pos.x = target.transform.position.x;
        pos.y = target.transform.position.y;
        this.transform.position = pos;
    }
}

C#スクリプトをカメラに割り付ける

1) Unityの画面に戻ります
2) HierarchyウィンドウからMain Cameraを選択します
3) ProjectウィンドウにあるC#スクリプトをInspectorウィンドウの空きスペースにドラッグ&ドロップ
(ここでは、CameraController を Main Camera の Inspector に追加します)

カメラをキャラクターに追従させる

先ほど作成したコードは、被写体をUnityの画面から設定できるようになっています。
先ほどのC#スクリプトに、ゲームオブジェクト「target」を定義しましたが、スクリプトに定義したGameObjectは、画面上で設定ができるようになります。

コード

public class CameraController : MonoBehaviour
{
    public GameObject target;    // ← これです

定義した名前の先頭を大文字にしたものが、画面に表示されます。
この場合は、Target の欄が追加されます。
(図)
また、上の「target」を「myTarget」などのように、複数の単語をつなげた場合、大文字の間にスペースを入れた名前が、画面に表示されます。
「myTarget」の場合は、「My Target」の欄が追加されます。

このように作っておくことで、スクリプトを変更しなくても被写体を画面上から入れ替えることができるようになります。
1) 上記のパラメータ欄に、Hierarchyウィンドウ上のオブジェクトをドラッグ&ドロップします。

動作の確認

Unityの画面から再生ボタンを押して、キャラクターを上下左右に動かして、カメラが追従する動きを確認します。カメラが追従しなかったり、画面の表示がおかしい場合は、次の項目を確認します。

カメラの制約

カメラを追従することによって表示がおかしくなる時は、キャラクター、マップ、カメラのZ座標を確認します。
Z座標は、2D画面上ではプラスが奥、マイナスが手前です。
デフォルトでは、TileMapやキャラクター用に作成されたオブジェクトのZ座標は0、MainCameraのZ座標は-10です。

カメラのZ座標はキャラクターに追従させない

3Dの場合、カメラをZ座標に追従させるのですが、2DではカメラをZ座標に追従させません。
上記のコードで、ゲームの開始時にMainCameraのZ座標だけを取り出して、画面が更新されるたびに使いまわしていますが、この処理が抜けると、MainCameraのZ座標がキャラクターよりも手前(マイナス値)でなくなる可能性があります。
コード

public class CameraController : MonoBehaviour
{
    private Vector3 pos;
    
    // Start is called before the first frame update
    void Start()
    {
        pos.z = this.transform.position.z;
    }

カメラの座標はZ座標も必ず正しく更新する(Vector2は使わない)

また、同じ理由で、カメラの座標を更新する時に、XYしか保存ができないVector2を使うことができません。(代入できますが、画面表示がおかしくなる可能性があります)

カメラをTileMapより手前(Z座標を小さく)

メインカメラとTileMapとの相性があるのでしょうか、少しクセがありますので注意が必要です。
キャラクターとTileMapは同じZ座標でもキャラクターが手前に描画されるのですが、MainCameraがTileMapと同じZ座標になると、キャラクターがTileMapより先に描画されてしまい、画面からキャラクターが見えなくなってしまいます。

表示範囲の変更

最後に、MainCameraを始めて触ったので、表示範囲を変更してみます。
1) Hierarchyウィンドウから Main Camera を選択
2) Inspectorウィンドウから Size を変更します。
 デフォルトでは 5 ですが、これを 7 に変えると、より広い領域を同時に表示しますが、キャラクターは小さくなります。

次回やること
カメラの追従は、いくつか作業が残っていますが、単純な追従だけでけっこうなボリュームになってしまったので、またどこかで挑戦します。
●キャラクターが画面の端に行くまでスクロールしない
●キャラクターがマップの端に行ったらスクロールしない

次回は、キャラクターを町、ダンジョンにそれぞれワープさせる方法を調べます。
●キャラクターのワープ

Unity学習#6 (Unity 2019.4.1f1) キャラクターのアニメーション処理

今回は十字キーで入力した方向にキャラクターが向く、歩く(アニメーション処理)に挑戦しました。
Animation と Animation Controller を使います。慣れれば直感的ですが、初心者には少しクセがあるように感じました(本投稿でまとめてあります)。

アニメーション用の画像を分割する処理は省略します。
オリジナリティを出すよりも、まず最低限ゲームとして完成させたいため、画像はAsset Storeから借りたものを使うことにしています(以前の記事を参考:Unity学習#4

構成

キャラクターをキー操作に応じてアニメーションを切り替える設定は、
それぞれの方向を向いている時のアニメーション(Animation)、アニメーションの切り替え(Animation Controller/Animator)、アニメーションを切り替えるオブジェクトで構成されます。アニメーションの切り替え(Animation Controller/Animator)の設定画面で、今どの方向を向いているかの状態を、Blend Treeという状態遷移図で管理します。
(図)

キャラクターの十字キーが、アニメーションの切り替えに伝達して、アニメーションが切り替わるまでの伝達経路は、以下のようになっています。
キー操作 → Animation Controller の Parameter → Animation Controller の状態を変更 → 状態毎に割り当てられたモーション (Animation) を再生
(図)

アニメーション(Animation)、アニメーションの切り替え(Animation Controller/Animator) は、それぞれ設定する順番が決まっていて、特にアニメーション(Animation)は、順番を守らないとキャラクターの画像を配置できませんので、その点注意が必要です。最初は手順が多く感じますが、分かってしまえば簡単かつ直感的で、感動すら覚えます。さすがUnityです。

今回は、第2回で作成したPlayerオブジェクトを、人型の画像に差し替えて、十字キーでキャラクターの向きを変えられるようにします。

準備

画像を準備する(省略)

画像を分割する(省略)

Animation Controller を作成する

Animation Controller オブジェクトの作成

(1) Projectウィンドウ > Assets を選択(作成場所の選択)
(2) Project > +▼ > Animator Controller を選択
(3) 新しいオブジェクトに名前を付けます
(今回はPlayer用のアニメーションなので、「Player」と名前を付けました)
(図)

オブジェクトへのアタッチ

(1) Hierarchyウィンドウで、アニメーションを付けたいオブジェクトを選択
(2) Projectウィンドウに作られた先ほどのAnimation Controllerを、Inspectorウィンドウへドラッグ&ドロップ
(今回は、Animation Controller「Player」を、第2回で作成してあった「Player」オブジェクトにアタッチしました。)
(図)

Animation Controller の設定

Animation Controller は、操作パネルから入力を受け付け、入力によってアニメーションを切り替えるための設定ができます。入力の受付をParameter、アニメーションの切り替えをLayersで行います。

Animatorウィンドウを開く

(1) Projectウィンドウで、先ほど作成したAnimation Controllerオブジェクトを選択
(2) Inspectorウィンドウで、Animation Controllerオブジェクト名の右にある「Open」ボタンをクリック
→Animatorウィンドウが表示されます(すでに表示されていれば何も起こりません)
(図)

入力設定 (Parameterタブ)

(1) Animatorウィンドウから、Parametersタブを選択
(2) +▼ > Float を選択
(3) パラメータの名前を入力
(今回は、XとYの入力値を受け取るため、dirX、dirYの2つを作成)
 ※作成したパラメータは、この後Layersに割り当てる必要があります。

アニメーションの切り替え設定 (Layersタブ)

状態遷移図の作成 (Blend Tree)

(1) Animatorウィンドウから、Layersタブを選択
(2) いくつかの箱が描画されているウィンドウを1回クリックし、ウィンドウが選択された状態にします。
(3) 画面上で右クリック > Create State > From New Blend Tree を選択
→新しい状態「Blend Tree」が作成されました。
※Blend Treeは複数の状態を一つにまとめるための大きい一つの状態定義です。

状態(Blend Tree)に名前を付ける

Blend Tree は、何の状態を表しているか分かるように、名前を付けます。
(1) 新しく作成された Blend Tree を1回クリック
(2) Inspectorウィンドウから名前を変更
(ここでは、Walk としておきます)

状態(Blend Tree)の設定

次に、Blend Treeの設定をします。
(1) 先ほど作成した Blend Tree をダブルクリック
→Animatorウィンドウのパンくずリストが Base Layer > Blend Tree に変わります
(図)
(2) 上記の状態で、Animator画面の余白を1回クリック
→InspectorウィンドウがBlend Treeの設定画面に変わります。
(図)
(3) Blend Type を 2D Simple Directional に変更します
(4) Parameter に、先ほど作成した2つのパラメータを割り当てます
(5) Motion欄 に4つのモーションを追加します:
 Motion欄の +▼ > Add Motion Field を選択
 これを4回繰り返します。
(図)

Animator の Inspectorウィンドウの設定は一旦ここまでで、次に各方向を向いている時のキャラクターアニメーションを作成していきます。

Animationを作成する

Projectウィンドウで、Assets が選択された状態で、
(1) Projectのすぐ下にある +▼ > Animation を選択します。
(2) 確定する前に、新しいアニメーションの名前を入力します。
(ここでは、正面、背面、右向きの3つのアニメーションを作成しますので、それぞれ、Player_front、Player_back、Player_right と名前を付けました)
(図)

Animationの設定

Animationは、Animation Controller にアサインされていない状態では、アニメーション設定ができません。そのため、先にAnimation を Animation Controller (Animator) と連結します。

Animation を Animation Controller (Animator) と連結する

(1) Projectウィンドウから、先ほどアタッチしたAnimation Controller (Animator) をダブルクリックします。
→ Animatorウィンドウが開きます(すでに開いている場合は何も起きません)
(2) 先ほどと同じ要領で、Blend Treeをダブルクリック > Animatorウィンドウ画面の空白部分を1回クリックし、InspectorウィンドウをBlend Treeの設定画面にします。
(3) 先ほど作成した3つのアニメーション(Animation)を、Blend TreeのMotion欄にドラッグ&ドロップします。
(今回は、右向きのアニメーションから左向きのアニメーションを作りますので、右向きのアニメーションは2か所にドラッグ&ドロップします)
(図)

Animation Controller (Animator) に、各 Animation の適用条件を設定する

それぞれのモーションの Pos X、Pos Y を、それぞれ以下の値にします。

Motion Pos X Pos Y
正面 0 -1
背面 0 1
右向き 1 0
(左向き) -1 0

(図)

動作原理はまだ理解不足なのですが、おそらく先ほどの2つのパラメータの値(ここではdirX、dirY)を使って、PosX、PosYとの距離が一番近い Motion (Animation) を適用する、または一番最後に適用した Motion (Animation) を適用し続ける、といった動きをしているのではないかと思われます。

Animationに画像を配置する

上記の設定が終わると、やっと各 Animation の設定が可能になります。
(1) Projectフォルダから、Animationオブジェクトをダブルクリックします
(2) Animationウィンドウが開きます
(3) タイムラインが書かれたウィンドウ上に、画像を配置します
※配置できるのは、オブジェクト名:Sprite と書かれた枠(少し濃いグレー)の枠内だけです。
※画像は、横▼マークがついているものでも、ついていないものでも配置できます。
(4) 配置が終わったら、Animationウィンドウ内の再生ボタンを押します
→Sceneビュー、またはGameビュー上でキャラクターがアニメーションしているのを確認できます
(5) タイムライン上の配置間隔を調節することで、アニメーションを早くしたり、遅くしたりできます

十字キーをAnimation Controllerに渡す

最後に、十字キーの入力を、Animation Controller の先ほどの2つのパラメータに渡す処理を追加します。
コード

public class Player : MonoBehaviour
{
    private float speed;
    private Vector2 input, scale;
    private Rigidbody2D rigidBody;
    private Animator animator;
    
    // Start is called before the first frame update
    void Start()
    {
        speed = 0.1f;
        rigidBody = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        Debug.Log("Start");
    }
    
    void Update()
    {
        input = new Vector2(
            Input.GetAxis("Horizontal"),
            Input.GetAxis("Vertical")
        );
    }
    
    private void FixedUpdate() {
        if (input == Vector2.zero){
            return;
        }
        rigidBody.position += input * speed;
        
        // キャラクターの左右反転
        scale = this.transform.localScale;
        if( input.x > 0 ){
            scale.x = 1;
        } else if( input.x < 0 ){
            scale.x = -1;
        }
        this.transform.localScale = scale;
        
        // アニメーション方向決定
        animator.SetFloat("dirX", input.x);
        animator.SetFloat("dirY", input.y);
    }
}

これで十字キーを入力すると、入力した方向にキャラクターが歩く動作をし続けるようになりました。