[UNITY][Ruby’s Adventure]#3 HP設定、回復とダメージ、敵の配置

前回までの記事

この記事はUNITYの公式チュートリアル『Ruby's Adventure』を日本語で解説している記事です。

↓↓記事一覧↓↓

#1 UNITY導入~キャラクター移動

#2 タイルマップ~物理演算

#3 HP設定~回復とダメージ~敵の配置 ←いまここ

#4 アニメーションの設定と適用

#5 攻撃とカメラ追従

#6 パーティクルとUI(HPゲージ)

#7 会話ダイアログ~音声~ビルド

(番外編)うまく動かない場合の対処法

アイテムを追加しよう

実際のチュートリアルページはこちら

HP回復アイテムを配置しよう

さて、キャラとマップができてきたところで、今度はアイテムを配置したいと思います。

まずはスクリプトRubyControllerを次のように書き加えてください。

public class RubyController : MonoBehaviour
{
    public int maxHealth = 5; //←ここ
    int currentHealth; //←ここ
    Rigidbody2D rigidbody2d;
    float horizontal;
    float vertical;

    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        currentHealth = maxHealth; //←ここ
    }

    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical") ;
    }

    void FixedUpdate()
    {
        Vector2 position = rigidbody2d.position;
        position.x = position.x + 3.0f * horizontal * Time.deltaTime;
        position.y = position.y + 3.0f * vertical * Time.deltaTime;
        rigidbody2d.MovePosition(position);
    }

    void ChangeHealth(int amount) //←ここから
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
        Debug.Log(currentHealth + "/" + maxHealth);
    } //←ここまで
}

//の箇所が書き加えた箇所です。順番に説明します。

まずここ。

    public int maxHealth = 5;
    int currentHealth;

public」は、定義した変数をUNITYのInspectorウインドウで編集するためのコードです。これを書くと、スクリプトを貼り付けたオブジェクトのInspectorウインドウで値を編集できるようになります(UNITY側で値を変えてもスクリプトは書き換わりません)。

maxHealth」「currentHealth」は見てのとおり、Rubyの最大HP、現在HPの変数ですね。

int」は整数を定義するための型です。このゲームではHPが小数点単位で徐々に減るのではなく、整数単位で減っていくようにするために「int」を使っています。

maxHealth = 5」と記述したので、最大HPの初期値は「5」になります。

 

次にここ。

    void Start()
    {
        rigidbody2d = GetComponent<Rigidbody2D>();
        currentHealth = maxHealth; //←ここ
    }

「Start()」に「currentHealth = maxHealth(現在HPに最大HPを入れるよ)」と書いてあります。つまり、開始と同時に現在HPが最大になるわけですね。

 

次にここ。

    void ChangeHealth(int amount)
    {
        currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
        Debug.Log(currentHealth + "/" + maxHealth);
    }

ここはHPの増減値を制御するメソッドです。

ChangeHealth」はこのメソッドを適当に解りやすく名付けたコードです。

amount(量)」はHPの増減値を入れるための変数です。

Mathf.Clamp」は値を指定範囲内に収めるための関数です。
「範囲内に収めた結果 = Mathf.Clamp(増減値,最小値,最大値);」という風に使います。

(currentHealth + amount, 0, maxHealth)」となってるので、増減したHPが0~MAXのなかに収まるようにしてるわけですね。

Debug.Log(currentHealth + "/" + maxHealth);」は解りますね。デバッグログに「現在HP/最大HP」と表示させるコードです。

 

以上により、まずは最大HPをpublicにしたことによりUNITY画面のInspectorウインドウで最大HPを手軽に調整できるようになりました。

 

また、「ChangeHealth」が呼び出されたときに、HPが増減するようになりました。

オブジェクトにトリガーを設定しよう

それでは、「回復アイテムを獲得したらChangeHealthが呼ばれて現在HPが回復する」処理を実装しましょう。

まずはProjectウインドウ内の「Art」→「Sprites」→「VFX」フォルダにある画像「CollectibleHealthを選択し、Inspectorウインドウ内でSprite Editorから画像サイズを確認してPixel Per Unitに反映してください。

できたら、画像ファイルをHierarchyウインドウにドラッグ&ドロップしてオブジェクト化してください。このイチゴが回復アイテムになります。

次に、そのオブジェクトに「Rubyと重なったらChangeHealthを呼び出す」ためのトリガー(きっかけ)を追加します。

トリガーを追加するには、イチゴのオブジェクトにコンポーネントBox Collider 2Dを追加し、そのなかのIs Triggerにチェックを入れるだけです。

Box Collider 2Dを付与するだけではRubyを重ねることができませんが、Is Triggerにチェックを入れることで重なることができ、かつ当たり判定がトリガーとして使える枠になります。

これでトリガーの準備はOKです。

スクリプトでトリガーの内容を指定しよう

まず、このトリガーで何をしたいのかを考えましょう。

このトリガーに処理させたいことを整理すると、次のようになります。

  • 自身の当たり判定(Collider 2D)にRubyの当たり判定(Collider 2D)が重なったとき、ChangeHealthをamount 1で呼び出して(つまりRubyのHPを1回復させて)から、自身は消える。
  • 自身の当たり判定(Collider 2D)にRuby以外の当たり判定(Collider 2D)が重なっても、何も起きない。

大きく分けると、この二つだと思います。

この処理を実現するために、イチゴ用のスクリプトを作りましょう。

Projectウインドウ内の「Scripts」フォルダに新たにC#スクリプトファイルを作成し、「HealthCollectible」と名付けてください。

次に、Hierarchyウインドウのイチゴオブジェクトを選択し、Inspectorウインドウにいま作ったスクリプトをドラッグ&ドロップで貼り付けてください。

で、そのスクリプトファイルを次のように書き換えます。

public class HealthCollectible : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D other)
    {
        RubyController controller = other.GetComponent<RubyController>();
        if (controller != null)
        {
            controller.ChangeHealth(1);
            Destroy(gameObject);
        }
    }
}

OnTriggerEnter2D()」は「判定内に何かが侵入したらトリガー発動」という処理をするためのコードです。

括弧()内には「何が入った時にトリガーを発動するか」という引数を書くのですが、「(Collider2D other)」と書くと「(別の)Collider2Dが侵入したら発動」という処理になります。

で、トリガーが発動したら行う内容に「RubyController controller = other.GetComponent<RubyController>();」と書いてあります。

まずここ。

        RubyController controller = other.GetComponent<RubyController>();

これは「侵入したオブジェクト(other)のなかの(ドット.)コンポーネント(GetComponent)のRubyControllerスクリプト<RubyController>)を取得して、変数「controller」に入れるよ」という意味です。

続いてここ。

        if (controller != null)
        {
            controller.ChangeHealth(1);
            Destroy(gameObject);
        }

まず!=」は「等しくない」という意味です

つまり「if (controller != null)」は、「もしも(if)変数controllerがカラッポ(null)ではない(!=)場合、大括弧{}内の処理を実行するよ」という意味です。

次に「controller.ChangeHealth(1);」は「変数controllerのなかのChangeHealthを値1で呼び出すよ」という意味です。

これは変数controllerにスクリプトRubyControllerが入っていた場合(=そのオブジェクトがRubyの場合)の処理で、元のコードが「ChangeHealth(int amount)」となっていたので、「ChangeHealth(1)」と書くと、amount(量)を1として処理してくれるわけですね。

次に「Destroy(gameObject);」ですが、これは見たままで「このゲームオブジェクトを削除する」という意味です。この書き方も非常によく使うので覚えておいてください。

 

さて、このスクリプトでRubyのHPを回復させることができるでしょうか?

スクリプトを保存してUNITYに戻ってみると、

あれ、なんかエラーが出ちゃいましたね。

これは『「HealthCollectibleスクリプト」から「RubyControllerスクリプトのChangeHealth()」にはアクセスできないよ』というエラーです。

さて、この問題を解決するためには、UNITY…というか、C#の根本的な部分を理解する必要があります。

public と private とアクセス制限

実はこのチュートリアルでは、普通だったらメチャクチャ使うのに、たまたま登場していなかったコードがあります。

それは「private」という記述です。これは本来死ぬほど使うコードなので、このチュートリアルで登場していなかったのは悪い意味で奇跡だと思います。

private」は「この変数(または関数)は、このスクリプト(class)のなかだけで使うよ」という意味です。

例えば「private int currentHealth;」と書くと「currentHealthという整数の入る変数を定義するよ」という意味ですが、最初に「private」と書かれているので、この変数(または関数)を取得したり編集したりできるのはそれが書かれたスクリプト(class)内だけです。

他のスクリプト(class)から使いたい場合は「public」と書きます。

以前、「public」はInspectorウインドウで編集できるようにするためのものと説明しましたが、実は「他のスクリプトから編集できるようにする」という意味もあるのです…というか本来はこっちがメインの機能です。

で、いままでは変数や関数(メソッド)を定義するときに「private」とも「public」とも書いていませんでしたよね。

実は、何も書かなかった時は自動的に「private」になります。

 

…ということは!

RubyControllerスクリプトのChangeHealth()」のコードをもう一度確認してみましょう。

void ChangeHealth(int amount)
{
    currentHealth = Mathf.Clamp(currentHealth + amount, 0, maxHealth);
    Debug.Log(currentHealth + "/" + maxHealth);
}

void ChangeHealth(int amount)」で「ChangeHealth()」という関数(メソッド)を定義していますが、「private」とも「public」とも書いていないので、この関数は「private」です。

そのため、他のスクリプトである「HealthCollectibleスクリプト」から「ChangeHealth()」にアクセスすることはできないのです。

ということで、次のとおり書き加えましょう。

public void ChangeHealth(int amount)

これで他のスクリプトからアクセスできるようになりました。

これで試動すると、エラーが消えましたね。

全部publicにしちゃえば?

…って思ったんじゃないですか?

確かに、全ての変数や関数を「public」にすれば、他のスクリプトから呼び出せるし便利なように思えます。

しかし、「public」には様々なデメリットがあります

public」にすると他の全てのスクリプトから書き換えることが可能なので、それが「どのタイミングで」「どのスクリプトから」変更されたのか探すのがわからなくなって、原因不明のエラーが発生する可能性があります。

なので、必要なとき以外は全て「private」にしておいた方が安全なのです。

 

…で、ちょっとお願いがあります。RubyControllerスクリプトを次のように書き加えてください。

public int health
{
    get
    {
        return currentHealth;
    }
}
int currentHealth;

次に、HealthCollectibleスクリプトのif文のあたりを次のように書き加えてください。

if (controller != null)
{
    if (controller.health < controller.maxHealth)
    {
        controller.ChangeHealth(1);
        Destroy(gameObject);
    }
}

これは何をしているかというと…

…すみません、私にはよくわからなったです

何回読んでもhealthという関数を定義して同じチェック(現在HPがマックスHPを下回っていた場合)を2回しているようにしか見えません。

これはプロパティという記述法を使っているそうなのですが、詳しく知りたいかたはこのあたりを読んでください

いや、プロパティの使い方自体は理解できるのですが、このチュートリアルでなぜ今これが必要なのか、ちょっと私には理解できませんでした…。

スポンサーリンク

ダメージと敵を追加しよう

実際のチュートリアルページはコチラです。

ダメージ床を配置しよう

さて、気を取り直して…

ゲームに踏んだらダメージを受ける床を追加しましょう。

この項では↓こんな機能を実装してみたいと思います。

  • Rubyが踏むとHPが1減るダメージ床を追加。
  • Rubyは静止していても、ダメージ床を踏んでいる間はHPが1減り続ける。

まずは、Projectウインドウ内の「Art」→「Sprites」→「Environment」→「」をHierarchyウインドウに追加してください。

これです。2D Spriteになってるかどうかチェックして、Pixel Per Unitを調整することを忘れずに。

次に、このオブジェクトを選択した状態でInspectorウインドウからコンポーネントBox Collider 2Dを追加し、その枠内の「Is Trigger」をONにしてください。これでこのオブジェクトが重なることのできるトリガーになりました。

次に、Projectウインドウ内のScriptsフォルダでC#スクリプトファイルを作成し、「DamageZone」と名付けてください。

で、その中身をこんな風に書き換えてください。

public class DamageZone : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D other)
    {
        RubyController controller = other.GetComponent<RubyController >();
        if (controller != null)
        {
            controller.ChangeHealth(-1);
        }
    }
}

さて、これは先ほど作った回復アイテムと見比べれば内容が理解できるかと思います。

このオブジェクトにRubyが侵入(Enter)したらHPが-1されるようにされていますね。

これで「Rubyが踏むとHPが1減るダメージ床を追加」はOKです。

トリガー発動条件を変更しよう

しかし、これではダメージを受けるタイミングが「侵入した瞬間のみ」なので、「Rubyが静止していても、踏んでいる間はHPが1減り続ける」ものにはなっていません。

そこで、このスクリプトを一カ所だけ書き換えます。

void OnTriggerStay2D(Collider2D other)

トリガーの発動条件を「OnTriggerEnter2D(侵入したときにトリガー発動)」から「OnTriggerStay2D(侵入し続けているあいだ)」に変更しました。このように、トリガーには様々な種類があることを覚えておきましょう。

これでダメージ床側の設定は終わりですが、今度はRuby側の設定も変更する必要があります。

UNITY画面に戻ってオブジェクトRubyを選択し、InspectorウインドウからRigidbody 2D枠内の→「Sleeping Mode」を「Start Awake(侵入時だけ判定)」から「Never Sleep(常に判定し続ける)」に変更してください。

これでRubyColliderに当たっている間、常に判定し続ける状態になりました。これもかなり重要な設定なので覚えておきましょう。

Rubyに無敵時間を追加しよう

続いて、「Rubyはダメージを受けると2秒間無敵になる」機能を実装しましょう。

RubyControllerスクリプトを開いてください。

まず、「public int maxHealth = 5;」の下に、次のようなコードを書き加えます。

public int maxHealth = 5;
public float timeInvincible = 2.0f; //←ここ
bool isInvincible; //←ここ
float invincibleTimer; //←ここ

public float timeInvincible = 2.0f;」は無敵時間を定義するための変数です。publicにしてInspectorウインドウで編集できるようにし、初期値として2.0fを入れています。

bool isInvincible;」について、まず「bool」は「この変数にはtrue(はい)かfalse(いいえ)を入れるよ」という意味の型です。つまり、ここで定義している「isInvincible」は無敵になったかどうかを判定するフラグですね。

float invincibleTimer;」は、無敵状態の経過時間を計測するための変数です。「○○秒間、○○する」という処理の際には必ずこういうタイマーみたいな変数を使うので、使い方を覚えましょう。

timeInvincible(タイム・インビンシブル)」と
invincibleTimer(インビンシブル・タイマー)」という変数名、超わかりにくいですよね…。
元のチュートリアルがこうなっているので一応このまま進みますが、自分でゲーム開発するときはこういう解りにくい変数名は避け、例えば「invincibleLimitTime(無敵限界時間)」「invincibleCountTimer(無敵計測タイマー)」のように、パッと見て見間違えないような変数名を心がけましょう。日本人だけで開発するなら「mutekiGenkaiJikan」みたいに日本語にしても全然OKだと思います。

続いて、この定義を使用してメソッドを書きます。

ChangeHealth()」の波括弧{}のなかに次の関数(メソッド)を書き加えてください。

if (amount < 0)
{
    if (isInvincible)
    return;
    isInvincible = true;
    invincibleTimer = timeInvincible;
}

if」は「括弧()内だった時に処理する」メソッドでしたね。

amount」はChangeHealth()内で定義した変数で、体力増減値が入っているものでしたね。

つまり、「if (amount < 0)」は「体力増減値が0未満(マイナス)だったら処理する」メソッドです。

少し行を飛ばして、「isInvincible = true;」は、さっきbool型で定義した変数「isInvincible」に「true(はい)」を入れています。つまり、無敵フラグをONにしている=無敵時間を開始しているわけですね。

invincibleTimer = timeInvincible;」は、変数「invincibleTimer無敵時間計測タイマー)」に「timeInvincible無敵限界時間)」を入れています。

つまり、どこかに「無敵時間計測タイマーを時間ごとに減らしていき、0になったら無敵時間終了」というメソッドを追加すればうまくいきそうですね。

一応、チュートリアルではカウントダウン形式になっていたのでこの方法で進めますが、
もちろん、「0からカウントアップして無敵限界時間を超えたら、無敵を終了して計測タイマーを0にする」という形式でも全く問題ありません。
自分でゲーム開発する際はお好みで。

 

で、上の行に戻って「if (isInvincible)」「return;」について、

if (isInvincible)」は「もし(if)無敵フラグがON(isInvincible)だったら、returnこのメソッドはここで終わり)」という意味です。つまり、これがメソッド内の最初に書いてあるので、既に無敵の場合は「無敵になる処理(returnよりも下)」がされたりしないようにしてるわけですね。

return」はメソッドを強制終了するためのコードで、割と使うので覚えておきましょう。

 

…あれ? と思ったかた。そうです。ちょっと書き方が変ですよね。今までならここは

    if (isInvincible)
    {
        return;
    }

こんな風に波括弧{}で囲んでいたはずです。

実は、if文は処理するコードがひとつの場合、波括弧{}が無くても動きます

で、チュートリアルでは波括弧{}が省略されているのでそのように説明したのですが、個人的にはイキって波括弧{}を省略してもエラーの原因になるだけなので、絶対に波括弧は{}使った方がいいと思います

損するのは自分なので、イキらないようにしましょう

 

で、続いて「Update()」の波括弧{}のなかに次の関数(メソッド)を書き加えてください。

if (isInvincible)
{
    invincibleTimer -= Time.deltaTime;
    if (invincibleTimer < 0)
    isInvincible = false;
}

これは先ほど言った「無敵時間計測タイマーを時間ごとに減らしていき、0になったら無敵時間終了」というメソッドです。

まず「if (isInvincible)」と書いてあるので、無敵になっている場合のみ計測タイマーが動きます。Update()内に書いてあるので、これが毎フレームチェックされるわけですね。

次に「invincibleTimer -= Time.deltaTime;」は、「invincibleTimer(無敵時間計測タイマー)」を「毎秒Time.deltaTime減らしていく-=)」という意味です。これがカウントダウンですね。

次に「if (invincibleTimer < 0) isInvincible = false;」は、先ほども登場した波括弧{}が省略されたif文ですね。内容は「invincibleTimer(無敵時間計測タイマー)が0未満になったら無敵フラグをOFF(無敵解除)」という意味です。

完成したスクリプトは忘れずに保存しておきましょう。

オブジェクトをタイル状に配置しよう

さて、せっかく完成したダメージ床なので、たくさん並べてゲームをスリリングにしたいですよね。

しかし、オブジェクトを選択したうえでTキーを押し、矩形ツールでサイズを大きくすると見栄えがとても悪いです。

かといってオブジェクトを複製すると処理が重くなるし…

そんなときに使える機能が「タイル化」です。

まずはHierarchyウインドウのダメージ床オブジェクトを選択し、Inspectorウインドウ内のSprite Renderer→Draw Modeを「Tiled」に変更し、

新たに展開された項目からTile Modeを「Adaptive(順応)」に変更します。

すると↑こんな警告が出るので、Projectウインドウ内のダメージ床の元画像を選択し、InspectorウインドウからMesh Typeを「Full Rect」に変更してください。

以上で、オブジェクトのサイズをTキーで調整してみると…

以上のスクリプトを保存して実行すれば、ダメージ床の完成です!

スポンサーリンク

敵を追加しよう

敵をインポートしよう

さて、いよいよゲームに敵を登場させます。

今回は「触れたらダメージを受ける」敵を作るので、ある意味「動くダメージ床」という感じの物を作ればよさそうです。

まずはこの画像をダウンロードしてください。

↑これです。

次に、いつものやつをやります。

  • Artフォルダにインポート
  • 解りやすくファイル名を「Robot」に変更
  • Texure TypeがSprite(2D and UI)になっているかチェック
  • Pixel Per Unitを調整
  • Hierarchyウインドウにドラッグ&ドロップしてオブジェクト化
  • コンポーネントSprite RendererのSprite Sort PointをPivotに変更
  • 元画像のPivotをBottomに変更
  • オブジェクトにコンポーネントRigidbody 2Dを追加
  • Gravity Scaleを0にする
  • Freeze RotationのZをチェック(回転防止)
  • コンポーネントBox Collider 2Dを追加
  • Colliderの形を調整
  • オブジェクトをPrefabフォルダにドラッグ&ドロップしてPrefab化

もう慣れたもんですね。

ちなみに、このゲームは壊れたロボットをRubyが直しながら進むというアクションゲームです。

なので、この敵は壊れたロボットであり、Rubyはこいつらに歯車を投げて直すことで敵を無力化していきます。このあたりを理解しないと次の章でつまづくので覚えておいてください。

ただし、「歯車投げ」では解りにくいので、このチュートリアルでは便宜上、歯車を投げることを「攻撃」と書きます。

まずは敵が自動で動くようにしよう

さて、敵が自動で動くためのスクリプトファイルを作ります。

まずはテスト用に「自動で右または上に動き続ける」ようにして、「右か上はInspectorウインドウで切り替えられる」ようにしたいと思います。

ScriptsフォルダでC#スクリプトファイルを作成、「EnemyController」と名付けて、次のように書き換えます。

public class EnemyController : MonoBehaviour
{
    public float speed;
    public bool vertical;
    Rigidbody2D rigidbody2D;

    void Start()
    {
        rigidbody2D = GetComponent<Rigidbody2D>();
    }

    void FixedUpdate()
    {
        Vector2 position = rigidbody2D.position;
        if (vertical)
        {
            position.y = position.y + Time.deltaTime * speed;
        }
        else
        {
            position.x = position.x + Time.deltaTime * speed;
        }
        rigidbody2D.MovePosition(position);
    }
}

さて、これらのコードはほとんどこれまでに登場したコードなので、読めば理解できると思います。

 

まずは

    public float speed;
    public bool vertical;
    Rigidbody2D rigidbody2D;

ですが、

public float speed;」で移動速度の変数を(Inspectorウインドウで編集できるようにpublicで)定義し、

public bool vertical;」でタテに動くように切り替えるための変数を(Inspectorウインドウで編集できるようにpublicで)定義し、

Rigidbody2D rigidbody2D;」でコンポーネントRigitbody2Dを取得するための変数を定義しています。

 

次に

    void Start()
    {
        rigidbody2D = GetComponent<Rigidbody2D>();
    }

では、

rigidbody2D = GetComponent<Rigidbody2D>();」により、先ほど定義した変数「rigidbody2D」でコンポーネントRigidbody2Dを取得しています。

これは「Start()」内に書いてあるので、シーン開始と同時にコンポーネントを取得しています。

 

次に

    void FixedUpdate()
    {
        Vector2 position = rigidbody2D.position;
        if (vertical)
        {
            position.y = position.y + Time.deltaTime * speed;
        }
        else
        {
            position.x = position.x + Time.deltaTime * speed;
        }
        rigidbody2D.MovePosition(position);
    }

ですが、

物理演算の移動に関するものなので「FixedUpdate()」内に記述しています。

Vector2 position = rigidbody2D.position;」で変数positionを定義したうえでコンポーネントRigidbody2D内のpositionをその変数に入れています。

if (vertical)」は、変数vertical(タテに動くフラグ)がONだった場合に処理することを書くコードです。

その内容に「position.y = position.y + Time.deltaTime * speed;」とあり、これは先ほどposition変数に代入したコンポーネントRigidbody2D内のpositionのy軸(position.y)をInspectorウインドウで編集したspeed値×秒数(Time.deltaTime)で増やす=上に動かし続けるコードです。

次の「else」は初登場ですね。これは「if」の後に書けるメソッドで、直前の「if」で指定した条件に対して「そうでなかった場合」の処理を指定するためのメソッドです。

直前の「if」が「変数vertical(タテに動くフラグ)がONだった場合」という条件だったので、「そうでなかった場合(else)」は「タテに動くフラグがOFFだった場合」=「ヨコに動かしたい場合」の処理を書くメソッドとなっています。

その内容である「position.x = position.x + Time.deltaTime * speed;」は、見たまんまですね。先ほどのy軸をx軸に変えただけ、つまり右に動かし続けるコードです。

次の「rigidbody2D.MovePosition(position);」は、Rubyを動かした時にも書きましたね。変数rigidbody2d=コンポーネント「Rigidbody 2D」のなかの「MovePosition(移動制御)」に変数positionを適用させるよ、という意味です。

オブジェクトを動かすためにはRigidbody2Dのpositionに値を入れるだけではダメです。
必ず「Rigitbody2Dを入れた変数.MovePosition(Positionの値を入れた変数);」というコードを記述して、コンポーネント「Rigidbody 2D」のなかの「MovePosition(移動制御)」に値を送り込む必要があるのです。
忘れやすいので注意しましょう。

 

さて、以上のスクリプトを保存してUNITYに戻ると、次のような警告が表示されます。

Assets\Scripts\EnemyController.cs(10,17): warning CS0108: 'EnemyController.rigidbody2D' hides inherited member 'Component.rigidbody2D'. Use the new keyword if hiding was intended.

と書いてあります。警告なので無視しても動くのですが、これは何を警告しているのでしょうか?

結論からいうと、この警告は無視しても大丈夫です。

で、何を警告しているのかというと…若干複雑な話になりますが、割と重要な「クラス」に関する話なので、頑張って理解しておきましょう。ちょっと長くなるので↓枠内に書いておきますね。

警告「CS0108」は、「変数名が継承元クラスと重複してるけど大丈夫!?」という意味です。

まず「クラスclass)」とはなんでしょう?
classは本来「設計図」「まとまり」みたいな意味なのですが、UNITYでは基本的に一つのスクリプトファイルクラスのように使います。
public class △△ { }」と書くことで「{ } の中では△△の機能を使う」という意味になります。
UNITYでは、もともとMonoBehaviourというクラス(設計図)が用意されてるので、UNITYでC#ファイルを作ると、スクリプトの初めに必ず「public class (スクリプトファイル名) : MonoBehaviour」と書いてありますが、これは「このスクリプトclassでは、class MonoBehaviourの機能を使うよ」という意味です。
ここでいう「このクラスの機能を使うよ」が「継承」です。「クラスの機能を使う」ことを「継承する」と言います。

では、警告にあった「継承元クラス」とは何のことでしょう?
スクリプトEnemyControllerで継承したクラスは一つだけ。それは「MonoBehaviour」です。

先ほど述べたとおり、UNITYでは内部にもともと「MonoBehaviour」というスクリプト、つまり設計図が用意されており、全てのスクリプトは「MonoBehaviour」の機能を使用する感じになっています(public class(スクリプトファイル名) : MonoBehaviour)。
つまり、UNITY内部で用意されたスクリプト「MonoBehaviour」の中にある変数と、スクリプトEnemyControllerにある変数の名前が「重複してるけど大丈夫!?」と言っているわけです。

では、その「名前が重複してる変数」って、一体どれのことでしょう?
調べてみると、どうやら「MonoBehaviour」の中では「rigidbody2D」という名前の変数が定義されているらしいのです。つまり、UNITYでrigidbody2Dという名前の変数を定義すると、必ずこの警告が出ることになります。

…おや? それでは、なぜこの警告は無視してOKなのでしょうか?
変数名が重複してるなんて、すごいエラーが起きそうですよね。
実は、クラスの中で変数を呼ぼうとすると、継承元ではなく、現在のクラス内で定義した変数が優先的に呼ばれます。つまり、「MonoBehaviour」で定義されている変数rigidbody2Dを別のクラス(スクリプト)で使っても、「MonoBehaviour」内の変数は呼ばれないのです。

まあ、それでも常に警告が出ている状態って、嫌ですよね。
なので、あなたがゲーム開発をするときには「rigidbody2D」という変数名は使わないほうが無難です。適当に「rigidbody_2D」とかにしておきましょう。

 

…話が長くなったので、一回休憩しましょうか。

 

敵が一定時間ごとに左右(または上下)に動くようにしよう

…さて、話を戻します。

以上のスクリプトを保存すると、正常に敵が右または上に動き続けると思います。

次に、自動で左右(または上下)に動かしましょう。一定時間動いたら、自動で逆方向に動くようにすれば良さそうです。

スクリプトの最初のほうに、次のコードを追加します。

public class EnemyController : MonoBehaviour
{
    public float speed = 3.0f;
    public bool vertical;
    public float changeTime = 3.0f; //←これ

    Rigidbody2D rigidbody2D;
    float timer;                    //←これ
    int direction = 1;              //←これ

//が追加した箇所です。

public float changeTime = 3.0f;」で敵が方向転換するまでの時間の変数を(Inspectorウインドウで編集できるようにpublicで)定義し、

float timer;」で敵の移動時間を計測する変数を定義し、

int direction = 1;」で移動方向を入れる変数を定義し、初期値を1にしています。

 

次に、Start()内を次のように書き加え、さらにUpdate()も次のように追加します。

void Start()
{
    rigidbody2D = GetComponent<Rigidbody2D>();
    timer = changeTime;          //←これ
}

void Update()                    //←ここから       
{
    timer -= Time.deltaTime;

    if (timer < 0)
    {
        direction = -direction;
        timer = changeTime;
    }
}                               //←ここまで

timer = changeTime;」で計測時間(timer)に方向転換までの時間(changeTime)を入れています。この状態でカウントダウンさせれば、「timerが0になったときに方向転換する」という処理が作れそうですね。

次にUpdate()を追加し、次のコードを追加しています。

timer -= Time.deltaTime;」で、計測時間(timer)の値を1秒ごとに減らしています。

if (timer < 0)」は「もし計測時間(timer)が0を下回ったら処理する」コードを書くメソッドですね。

その内容にある「direction = -direction;」は、direction方向)を「マイナス-」にしています。先ほどdirection方向)の初期値を1にしたので、「-direction」と書くと「-1」になるわけですね。

次に、方向転換が終わったので「timer = changeTime;」で計測時間をリセットしています。

 

続いて、FixedUpdate()内のif文を次のように書き加えます。

    if (vertical)
    {
        position.y = position.y + Time.deltaTime * speed * direction; //←この最後のやつ
    }
    else
    {
        position.x = position.x + Time.deltaTime * speed * direction; //←この最後のやつ
    }

ここは敵を自動的に移動させているメソッドでしたね。

* direction;」で、の移動値にdirectionを乗算(*)しています。directionには「1」または「-1」が入っているので、移動値がプラス値になれば右(上)に、マイナス値になれば左(下)に移動してくれるわけですね。

ちなみに、元のチュートリアルページを見ると「direction;」が「direction;;」となっており、「セミコロン;」が2つ入っています。
これは恐らく誤りで、「セミコロンが2つ無ければいけないコード」なんて存在しません。
が、2つ書いてしまっていても特にエラーにはなりません。

以上のスクリプトを保存して試動すると、敵が自動で左右(上下)に動いてくれていることがわかります。

試動しながら方向や方向転換までの時間をInspectorウインドウで編集してチェックしてみましょう。

敵に当たったらRubyがダメージを受けるようにしよう

さて、敵を移動させることには成功したので、次にダメージを追加していきます。

ダメージ床と同じようにOnTriggerEnter2Dを使って「重なったらRubyのHPを1減らす」としてもよいのですが、敵はRubyと同じく地上を歩いてるので、「重なる」というより「ぶつかる」感じにしたいですよね。

そんな時はOnCollisionEnter2Dというメソッドを使用します。同じくEnemyControllerスクリプトに次のメソッドを書き加えてください(MonoBehaviourの中に、Start()、Update()、FixedUpdate()と並べて書き加えます)。

void OnCollisionEnter2D(Collision2D other)
{
    RubyController player = other.gameObject.GetComponent<RubyController>();

    if (player != null)
    {
        player.ChangeHealth(-1);
    }
}

見ればわかるとおり、ダメージ床の「OnTriggerEnter2D」が「OnCollisionEnter2D」に変えただけですね。

このように、当たり判定を使う場合は色々な種類があるので覚えておきましょう。

  • OnTriggerEnter2D … 判定に判定が侵入したとき(すり抜けたとき)発動
  • OnCollisionEnter2D … (Rigidbodyを持つ)判定と判定がぶつかったとき(すり抜けると発動しない)発動
  • OnTriggerStay2D … 判定と判定が重なっているあいだ発動し続ける
  • OnCollisionExit … 重なっている判定と判定が離れたとき発動

以上で完成です!

うまく動かなかったかたはこちらの記事を参照してください。

(ちょっと寄り道)アセットのインポートに注意しよう

さて、ここでちょっとチュートリアルから外れて寄り道しましょう。

このチュートリアルを始めるとき、Assets Storeから「Ruby's Adventure」というプロジェクトを無料ダウンロードして、UNITYのPackage Managerでインポートしましたよね。

で、アセットストアにはテクスチャやBGMなど、素材データもたくさんあるのでオススメだよ~という話をしました。

このように、便利な素材を外部からUNITYにインポートするということはよくあることです。

…が、インポートするアセットの規模が大きいと、インポートした瞬間にエラーが発生したりします。

例えば先に紹介した「Tile Palette」などUNITYが公式に配布しているものであっても、インポートしたプロジェクトと相性が悪くてエラーが発生するなんてことが稀にあります。

そのため、Package Managerでアセットをインポートする際には必要最低限にとどめておく、もし大規模なアセットを使うなら開発初期のうちにインポートしておくと良いでしょう。もちろん、開発に便利なアセットはどんどんインポートしてOKです!

 

次の記事はこちら

#4 アニメーションの設定と適用