MonoGameゲーム制作入門

MonoGameで色んなゲームやサンプルを作ってみましょう!!

基本的に、上にあるほうが簡単で、下に行くほど複雑になります。
(あまり当てにはなりません)


じゃんけんゲーム


シンプルなじゃんけんゲームです。
画像の出し方や、Randomの使い方、ゲームの状態遷移などの基本を学びます。

やってみる



MonoGameでマウスを使う方法


MonoGameでマウスを使う方法を学びます。

やってみる



まるばつゲーム


オーソドックスなまるばつゲームを作ります。

やってみる



ライツアウト(点灯パズル)

押した場所とその上下左右のマスがON/OFF切り替わるパズルゲームです。全てのマスを消すことができれば、クリア!




マインスイーパー

定番のパズルゲーム「マインスイーパー」を作ってみます。




ブロック崩し

定番のブロック崩しです。





動きに緩急を付ける(Easing, Tween)

Easing関数を使って、緩急の付いたリッチな動きを実現しよう!





倉庫番

定番パズルゲームの倉庫番です。マス単位でキャラクターを動かす良い練習になると思います。



グラディウスのオプションみたいなやつを色々作る

https://motoyamablog.blogspot.com/2018/06/monogame-gradius-option.html
色んな動きをするオプション(自機の子分みたいなやつ)を作ります。全7種類。




フラッピーバード風ゲーム


2014年頃に突発的な大ブームを巻き起こしたゲーム「フラッピーバード」風のゲームをMonoGameで作ってみましょう。

やってみる

グラディウスのオプションみたいなやつを色々作る


シューティングゲームにおいて、自機をサポートしてくれる子分的なやつ。オプションとかビットとか呼ばれるやつ。
今回はMonoGameで色んなオプション的なやつを作ってみます。
グラディウスなどの過去の名作で実際にあったオプションから、私が思いつきで作ったものまで、全部で7種類のオプションの作り方を紹介します。

動きに緩急を付ける(Easing, Tween)

動きに緩急を付けてワンランク上の見た目にする方法をやります。

EasingおよびTweenとは


このGIF画像では、クリックした場所にぱっちぃが0.5秒かけて移動しています。

まあ、普通ですね。




では、こちらはどうでしょうか?

こちらも、クリックした場所にぱっちぃが0.5秒かけて移動しています。それはさきほどと同じです。
しかし、明らかに見た目の印象が違います。

何が違うのでしょうか?
わかりますか?

上のGIF画像では、ぱっちぃの移動は常に一定の速度なのです。それに対し、下のGIF画像では、動きに緩急が付いています。動き始めは速く、終盤は減速してゆっくりになっています。そのおかげで、少ししっとりとした、オシャレな感じになるのです。

このように動きに緩急を付けることを「Easing(イージング)」や「Tween(トゥイーン)」と呼びます。

最近のアプリやゲームでは日常的に使用される技法です。
普段遊んでいるゲームで、UIなどに注目してみましょう。いまどきのゲームでこの技法を使っていないものはおそらく無いでしょう。


準備

では、MonoGameで作ってみましょう。

今回は、この画像を使うので、右クリックして保存しておいてください。

次に、Visual Studioで、新規Windows MonoGameプロジェクトを作成します。
この記事のサンプルコードでは「Easing」というプロジェクト名にしています。

Content.mgcbを開いて、上の画像 pacchi_32.png を登録してビルドしておいてください。



指定の時間で移動するプログラム

緩急を付ける前に、まずは「指定された場所まで、指定された時間をかけて移動する」プログラムを書きます。

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Easing
{
    public class Game1 : Game
    {
        // 状態種別
        enum State
        {
            Moving, // 移動中
            Stopped, // 停止
        }

        const int Duration = 30; // 移動にかける時間(フレーム数)

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        Texture2D texture; // テクスチャ
        MouseState prevMouse; // 1フレーム前のマウスの状態
        Vector2 currentPosition = Vector2.Zero; // 現在の位置
        Vector2 startPosition; // 移動の開始位置
        Vector2 endPosition; // 移動の終了位置
        int elapsedTime; // 移動の経過時間
        State state = State.Stopped; // 現在の状態

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            // マウスを表示する
            IsMouseVisible = true;

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
            texture = Content.Load<Texture2D>("pacchi_32"); // 画像読み込み
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            MouseState currentMouse = Mouse.GetState();

            // マウスが押された?
            if (prevMouse.LeftButton == ButtonState.Released && currentMouse.LeftButton == ButtonState.Pressed)
            {
                // 移動の開始位置を現在の位置とする
                startPosition = currentPosition;
                // 移動の終了位置をクリックされた場所とする
                endPosition = new Vector2(currentMouse.X, currentMouse.Y);
                // 経過時間を0にする
                elapsedTime = 0;
                // 状態をMovingにする
                state = State.Moving;
            }

            // 移動中のときの処理
            if (state == State.Moving)
            {
                // まだ終了時間ではない場合
                if (elapsedTime < Duration)
                {
                    // どれくらい時間が経過したのかを0~1で表す
                    float timeRate = (float)elapsedTime / Duration;
                    // 開始位置と終了位置の間のどこにいれば良いかをLerp(線形補間)で割り出す
                    currentPosition = Vector2.Lerp(startPosition, endPosition, timeRate);
                }
                // 終了時間の場合
                else
                {
                    // 終了位置にいく
                    currentPosition = endPosition;
                    // 状態をStoppedにする
                    state = State.Stopped;
                }

                // 経過時間をインクリメントする
                elapsedTime++;
            }

            // 次のフレームのために、今回のマウスの状態をとっておく
            prevMouse = currentMouse;

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            spriteBatch.Begin();

            // currentPositionに画像を描画する
            spriteBatch.Draw(texture, currentPosition + new Vector2(-16, -16), Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}



クリックした場所に、ぱっちぃの画像が移動するようになります。

今回は、線形補間という技法で移動を行っています。
やたらと難しそうですが、難しいのは名前だけです。

例えば・・・問題です!!

A君は10秒で20メートル歩くことができます。
5秒では何メートル歩けるでしょう!?























答えは10メートルですね。



当たり前ですね。

これが線形補間です。




まあ、実際はもう少しだけ話が複雑なことが多いですが。
例えば、こんな感じ。

問題です!!

B君の体重はもともと60kgでしたが、その後100日間で70kgに増えてしまいました。
B君の体重が一定の速度で増えていったと仮定すると、60kgのときから25日経過した時点での体重は何kgだったでしょう!?























答えは62.5kgですね。

簡単ですよね。
これが線形補間です。

要するに、一定の速度で変化する何かがあり、その始めと終わりの値がわかっていて、トータルの時間もわかっていれば、途中の値は計算で出せるよね、と。これをカッコつけて線形補間と呼んでいます。


線形補間は、ゲームプログラミング(グラフィック系のプログラミング)では重宝します。重宝しすぎます。

重宝しすぎるので、大抵の環境では、この線形補間を計算してくれる関数が用意されています。
大抵、Lerp(ラープ)という名前です(Linear interpolation:線形補間の略)。


で、今回のプログラムでは
Vector2.Lerp()
というのを使っています。
これは、Vector2型の値に対して線形補間を行うものです。
まあ、ただ単にXとYに対して個別に線形補間してるだけなんですが、自分でやるのも面倒なので、ありがたく使わせてもらいましょう。

Lerp関数の引数は、大抵、

第1引数:開始値
第2引数:終了値
第3引数:経過時間

となっています。第3引数の経過時間の指定方法は、大抵、0~1での指定となります。
例えば、10秒かけて移動する場合の5秒時点での位置を求めたかったら0.5を指定します。




Easing関数を使って緩急を付ける

では、いよいよ本題です。
動きに緩急を付けます。

まず、次の関数を追加してください。

        // 序盤が速く、徐々に減速する
        float EaseOutQuart(float t)
        {
            t -= 1f;
            return -1f * (t * t * t * t - 1);
        }

そして、Vector2.Lerp()を使って移動させている処理の直前に、1行追加します。

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            MouseState currentMouse = Mouse.GetState();

            // マウスが押された?
            if (prevMouse.LeftButton == ButtonState.Released && currentMouse.LeftButton == ButtonState.Pressed)
            {
                // 移動の開始位置を現在の位置とする
                startPosition = currentPosition;
                // 移動の終了位置をクリックされた場所とする
                endPosition = new Vector2(currentMouse.X, currentMouse.Y);
                // 経過時間を0にする
                elapsedTime = 0;
                // 状態をMovingにする
                state = State.Moving;
            }

            // 移動中のときの処理
            if (state == State.Moving)
            {
                // まだ終了時間ではない場合
                if (elapsedTime < Duration)
                {
                    // どれくらい時間が経過したのかを0~1で表す
                    float timeRate = (float)elapsedTime / Duration;
                    // イージング関数により緩急をつける
                    timeRate = EaseOutQuart(timeRate);
                    // 開始位置と終了位置の間のどこにいれば良いかをLerp(線形補間)で割り出す
                    currentPosition = Vector2.Lerp(startPosition, endPosition, timeRate);
                }
                // 終了時間の場合
                else
                {
                    // 終了位置にいく
                    currentPosition = endPosition;
                    // 状態をStoppedにする
                    state = State.Stopped;
                }

                // 経過時間をインクリメントする
                elapsedTime++;
            }

            // 次のフレームのために、今回のマウスの状態をとっておく
            prevMouse = currentMouse;

            base.Update(gameTime);
        }


たったのこれだけで、動きに緩急が付きます!

 

まるで魔法のようですね。

この魔法のような関数を発明したのは、Robert Pennerという人らしく、どうやらFLASHの開発者の方だとか。

上記の EaseOutQuart という関数および、ここから下のコードは、Robert Penner氏の著作物です。

オープンソースであり、MITライセンスおよびBSDライセンスなので、無料で自由に使えますが、使った作品を公開する場合はライセンス表記が必要なので、注意しましょう。




Easing関数

上記のEaseOutQuartという関数は一体何なのでしょうか?

EaseOutQuartは、0~1の範囲の値を受け取り、ちょびっとだけ変更して返却する関数です。

例えば、受け取った値をそのまま返却する関数があったとします。
        float Sample(float t)
        {
            return t;
        }
引数をx, 戻り値をyとすると、
y = x となります。

数学的にグラフにするとこうです




これに対して、EaseOutQuart関数の引数をx、戻り値をyとしたときのグラフはこうです


2つのグラフには共通点があります。
xが0のときは、yも0で、
xが1のときは、yも1。
というところです。

違うのは途中経過です。
EaseOutQuartのほうは、序盤、ぐんぐん変化して、徐々に失速し、終盤はほとんど変化しません。

この値の変化が、そのまま、今回のプログラムの緩急の付いた動きに表れているということです。



で、ここからがEasing関数のさらにすごいところなのですが、Easing関数は他にもたくさんの種類があり、関数を取り替えるだけで、簡単に見た目の印象を変えることができるのです。


今度は、少しクセのあるEasing関数を使ってみましょう。
次の関数を追加してください。
        // 序盤から速い。最後は一度、目標値を通り越してから、目標値に戻る。
        // overshoot : 通り越す量
        float EaseOutBack(float t, float overshoot = 1f)
        {
            float s = 1.70158f * overshoot;
            t -= 1f;
            return (t * t * ((s + 1f) * t + s) + 1f);
        }

 で、EaseOutQuart()を使っていた部分を、EaseOutBack()に取り替えます。
                    // イージング関数により緩急をつける
                    timeRate = EaseOutBack(timeRate);





勢いがすごくて、目的地を通り過ぎるような動きになりました!



このように、関数を取り替えるだけで、見た目の印象をがらっと変えることができるのがEasing関数のすごいところです。


他にも下記のように大量の関数があります
// 序盤が速く、徐々に減速する
float EaseOutQuad(float t)
{
    return -1f * t * (t - 2f);
}

// 序盤は遅く、徐々に加速する
float EaseInQuad(float t)
{
    return t * t;
}

// 序盤は遅く、中盤は速くなり、終盤はまた遅くなる
float EaseInOutQuad(float t)
{
    t *= 2f;
    if (t < 1) return t * t * 0.5f;
    t -= 1f;
    return (t * (t - 2) - 1) * -0.5f;
}

// 序盤が速く、徐々に減速する
float EaseOutCubic(float t)
{
    t -= 1f;
    return t * t * t + 1;
}

// 序盤は遅く、徐々に加速する
float EaseInCubic(float t)
{
    return t * t * t;
}

// 序盤は遅く、中盤は速くなり、終盤はまた遅くなる
float EaseInOutCubic(float t)
{
    t *= 2f;
    if (t < 1f) return 0.5f * t * t * t;
    t -= 2f;
    return (t * t * t + 2f) * 0.5f;
}

// 序盤が速く、徐々に減速する
float EaseOutQuart(float t)
{
    t -= 1f;
    return -1f * (t * t * t * t - 1);
}

// 序盤は遅く、徐々に加速する
float EaseInQuart(float t)
{
    return t * t * t * t;
}

// 序盤は遅く、中盤は速くなり、終盤はまた遅くなる
float EaseInOutQuart(float t)
{
    t *= 2f;
    if (t < 1) return 0.5f * t * t * t * t;
    t -= 2f;
    return -0.5f * (t * t * t * t - 2f);
}

// 序盤が速く、徐々に減速する
float EaseOutQuint(float t)
{
    t -= 1f;
    return t * t * t * t * t + 1f;
}

// 序盤は遅く、徐々に加速する
float EaseInQuint(float t)
{
    return t * t * t * t * t;
}

// 序盤は遅く、中盤は速くなり、終盤はまた遅くなる
float EaseInOutQuint(float t)
{
    t *= 2f;
    if (t < 1f) return 0.5f * t * t * t * t * t;
    t -= 2f;
    return 0.5f * (t * t * t * t * t + 2f);
}

// 序盤が速く、徐々に減速する
float EaseOutExpo(float t)
{
    return (float)(-System.Math.Pow(2, -10 * t) + 1);
}

// 序盤は遅く、徐々に加速する
float EaseInExpo(float t)
{
    return (float)(System.Math.Pow(2, 10 * (t - 1)));
}

// 序盤は遅く、中盤は速くなり、終盤はまた遅くなる
float EaseInOutExpo(float t)
{
    t *= 2f;
    if (t < 1f) return (float)(0.5 * System.Math.Pow(2, 10 * (t - 1)));
    t -= 1f;
    return (float)(0.5 * (-System.Math.Pow(2, -10 * t) + 2));
}

// 序盤から速い。最後は一度、目標値を通り越してから、目標値に戻る。
// overshoot : 通り越す量
float EaseOutBack(float t, float overshoot = 1f)
{
    float s = 1.70158f * overshoot;
    t -= 1f;
    return (t * t * ((s + 1f) * t + s) + 1f);
}

// 一度、開始値から逆走してから、目標値に向かって一気に動く。後ろにバックしてパワーを溜めてからはじけるようなイメージ。
// overshoot : 開始時にバックする量
float EaseInBack(float t, float overshoot = 1f)
{
    float s = 1.70158f * overshoot;
    return t * t * ((s + 1f) * t - s);
}

// EaseOutBackとEaseInBackの両方の特徴をあわせ持つ。
// 後ろにバックしてパワーを溜めて、はじけるように移動し、最後は一旦目標値を通り越してから、目標値に戻る。
// overshoot : 開始時にバックする量、および終了時に通り越す量
float EaseInOutBack(float t, float overshoot = 1f)
{
    float s = 1.70158f * overshoot;
    t *= 2f;
    if (t < 1f)
    {
        s *= (1.525f) * overshoot;
        return 0.5f * (t * t * ((s + 1f) * t - s));
    }
    t -= 2f;
    s *= (1.525f * overshoot);
    return 0.5f * (t * t * ((s + 1f) * t + s) + 2f);
}

// スーパーボールのように、目標地点でバウンドするような動き。
float EaseOutBounce(float t)
{
    if (t < (1f / 2.75f))
    {
        return 7.5625f * t * t;
    }
    else if (t < (2f / 2.75f))
    {
        t -= (1.5f / 2.75f);
        return 7.5625f * (t) * t + .75f;
    }
    else if (t < (2.5f / 2.75))
    {
        t -= (2.25f / 2.75f);
        return 7.5625f * (t) * t + .9375f;
    }
    else
    {
        t -= (2.625f / 2.75f);
        return 7.5625f * (t) * t + .984375f;
    }
}

// バネのようにビヨビヨーンとなる動き。
// overshoot : 振れ幅の大きさ。
// period : 周波数。値が小さいほど小刻みに揺れ、値が大きいほどゆったりと揺れる。
float EaseOutElastic(float t, float overshoot = 1f, float period = 1f)
{
    period /= 4f;
    float s = period / 4f;

    if (overshoot > 1f && t < 0.4f)
        overshoot = 1f + (t / 0.4f * (overshoot - 1f));

    return (float)(1 + System.Math.Pow(2, -10 * t) * System.Math.Sin((t - s) * (2 * System.Math.PI) / period) * overshoot);
}

本当は、まだいくつかあるのですが、個人的に「これは使わんだろ・・・」というものは除外しました。


これだけ大量にあると、どれを使えば良いのか、途方にくれてしまいます。一個ずつ全て試すわけにもいきません。
そこで、早見表が用意されています。


※画像クリックで、詳細ページが開きます


基本的に、EaseIn~は序盤が遅いやつで、
EaseOut~は終盤が遅いやつです。

私がよく使うのは・・・

  • EaseOutQuart
  • EaseInOutCubic
  • EaseOutBack
  • EaseInBack
  • EaseOutElastic
あたりですかね。

いろいろ、試してみて、お気に入りを見つけてみてください!





移動以外にも使えるぞ

今回のサンプルコードでは、移動に対してEasing関数を使いましたが、移動以外に対してEasing関数を使うことも、よくあります。
例えば、拡大/縮小や回転などに使います。





いろいろ試してみてください!



あ、ちなみに、ゲームエンジンやスマホアプリ開発環境などには、最初からこのEasingを実現する機能が用意されていることが多いので、自作する前にまずは調べましょう。

例えば、Unityだと、DOTweenやLeanTweenなど。

もちろん、MonoGameには無いです・・・


↓Easing関数のライセンスです。Easing関数を利用したものを公開する場合は、どこかに下記のライセンス表記が必要です。

Terms of Use: Easing Functions (Equations)
Open source under the MIT License and the 3-Clause BSD License.

MIT License
Copyright © 2001 Robert Penner

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

BSD License
Copyright © 2001 Robert Penner

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.




倉庫番

MonoGameで、往年の名作パズルゲーム「倉庫番」を作ってみましょう!




ライツアウト


LightsOut(ライツアウト)とか点灯パズルゲームとか呼ばれる定番のパズルをMonoGameで作ってみましょう。
光っているマスと光っていないマスが並んでいて、クリックすると、クリックしたマスを中心とした5マス(クリックしたマスとその上下左右)の点灯/消灯が切り替わります。全てのマスを消灯させることができればゲームクリアです。
シンプルなルールですが、結構やりごたえがあります。

慣性オプション

慣性のあるオプションを作ります。

「慣性」とは「動いているものは動き続けようとする」という物理において当たり前の法則のことですね。

今回はその慣性を持ったオプションを作ります。

実はグラディウスIIIには「スネークオプション」というオプションがあり、そいつが慣性を持ったような動きをするのですが・・・しかしながらこいつが非常に厄介で、まあ一言でいうと意味わからん動きをするのです。


あまりにも思い通りに動かないので、

「このオプションを作ったのは誰だあっ!!」と
海原雄山ばりに怒鳴ることうけあい。

スネークオプションは、プレイヤーたちの間では完全に「使えない子」扱いでした・・・


だいぶ話が逸れましたが、今回は、グラディウスIIIのスネークオプションとは違って、割と素直に動くものを作ります。



サンプルコード

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Inertial
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        // プレイヤー移動速度
        const float PlayerSpeed = 7.0f;
        // オプションの距離
        const float OptionDistance = 100f;
        // 減衰率
        const float Dump = 0.95f;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // ぱっちぃのテクスチャ
        Texture2D texturePacchi;
        // かまトゥのテクスチャ
        Texture2D textureKamatoo;
        // プレイヤーの位置
        Vector2 playerPosition = new Vector2(200, 200);
        // オプションの位置
        Vector2 optionPosition;
        // オプションの速度
        Vector2 optionVelocity;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像を読み込む
            texturePacchi = Content.Load<Texture2D>("pacchi_32");
            textureKamatoo = Content.Load<Texture2D>("kamatoo_32");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // 上下左右が押されたらプレイヤーを移動させる
            if (Keyboard.GetState().IsKeyDown(Keys.Left))
            {
                playerPosition.X -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
            {
                playerPosition.X += PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Up))
            {
                playerPosition.Y -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Down))
            {
                playerPosition.Y += PlayerSpeed;
            }

            // オプションの移動前の位置をとっておく
            Vector2 prevOptionPosition = optionPosition;
         
            // 速度の分だけ移動させる
            optionPosition += optionVelocity;

            // オプションが親から離れている場合、位置補正(範囲内へのひっぱり)が必要。
            if (Vector2.Distance(optionPosition, playerPosition) > OptionDistance)
            {
                // 親から見たオプションへの方向ベクトルを算出
                Vector2 direction = optionPosition - playerPosition;

                // 正規化(長さを1に)
                direction.Normalize();

                // 位置を補正(ひっぱり)
                optionPosition = playerPosition + direction * OptionDistance;

                // 今回移動した量を速度として保持しておく
                optionVelocity = optionPosition - prevOptionPosition;

                // 速度を減衰させる。そうしないと、グワングワン回転して動いてしまう
                optionVelocity *= Dump;
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            spriteBatch.Begin();

            // オプションを描画
            spriteBatch.Draw(textureKamatoo, optionPosition, Color.White);

            // プレイヤーを描画
            spriteBatch.Draw(texturePacchi, playerPosition, Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}







解説

ある程度離れたら引っ張ってくるという点において、引きずりオプションのときとかなり似てます。
今回は、引っ張った際に、勢い(velocity)を保存して、その後も勢いによって動き続けるようにしています。

また、減衰率(Dump)によって、勢いを弱める工夫もしています。Dump=0.95なので、毎フレーム、勢いが95%に減っていき、そのうち止まります。これが無いとぐわんぐわん動いて大変です(試しにDumpをかけるのをやめてみればわかります)。





オプションを増やすともっと面白い

上記はコードを単純にするために、オプションの個数が1つですが、オプションの数が多いと、もっと面白いです。




かなり自由自在に動かせます。振り回すこともできます。
グラディウスIIIのスネークオプションもこれくらい素直だったら・・・と思いましたが、ここまで自由に動かせると、強すぎてバランス崩壊かも😅
いやー、しかし、グラディウスIIIは難しすぎてバランス崩壊してるから、これくらい強い味方がいて、トントンかな!?

話がだいぶ逸れました。
オプションの個数を増やすのにぜひチャレンジしてみてください。

なるべく自力でチャレンジしてみてほしいですが、一応、サンプルコードも載せておきます。


using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Generic;

namespace InertialMultiple
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        // プレイヤー移動速度
        const float PlayerSpeed = 7.0f;
        // オプションの距離
        const float OptionDistance = 50f;
        // 減衰率
        const float Dump = 0.93f;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // ぱっちぃのテクスチャ
        Texture2D texturePacchi;
        // かまトゥのテクスチャ
        Texture2D textureKamatoo;
        // プレイヤーの位置
        Vector2 playerPosition = new Vector2(200, 200);
        // オプションの位置(複数個分)
        List<Vector2> optionPositions = new List<Vector2>();
        // オプションの速度(複数個分)
        List<Vector2> optionVelocities = new List<Vector2>();

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            // オプションの情報を5個分用意
            for (int i = 0; i < 5; i++)
            {
                optionPositions.Add(playerPosition);
                optionVelocities.Add(Vector2.Zero);
            }

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像を読み込む
            texturePacchi = Content.Load<Texture2D>("pacchi_32");
            textureKamatoo = Content.Load<Texture2D>("kamatoo_32");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // 上下左右が押されたらプレイヤーを移動させる
            if (Keyboard.GetState().IsKeyDown(Keys.Left))
            {
                playerPosition.X -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
            {
                playerPosition.X += PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Up))
            {
                playerPosition.Y -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Down))
            {
                playerPosition.Y += PlayerSpeed;
            }

            for (int i = 0; i < optionPositions.Count; i++)
            {
                // オプションの移動前の位置をとっておく
                Vector2 prevOptionPosition = optionPositions[i];

                // 速度の分だけ移動させる
                optionPositions[i] += optionVelocities[i];

                // 親の位置。最初のオプションの親はプレイヤー。それ以外のオプションは自分のひとつ前のオプション
                Vector2 parentPosition = i == 0 ? playerPosition : optionPositions[i - 1];

                // オプションが親から離れている場合、位置補正(範囲内へのひっぱり)が必要。
                if (Vector2.Distance(optionPositions[i], parentPosition) > OptionDistance)
                {
                    // 親から見たオプションへの方向ベクトルを算出
                    Vector2 direction = optionPositions[i] - parentPosition;

                    // 正規化(長さを1に)
                    direction.Normalize();

                    // 位置を補正(ひっぱり)
                    optionPositions[i] = parentPosition + direction * OptionDistance;

                    // 今回移動した量を速度として保持しておく
                    optionVelocities[i] = optionPositions[i] - prevOptionPosition;

                    // 速度を減衰させる。そうしないと、グワングワン回転して動いてしまう
                    optionVelocities[i] *= Dump;
                }
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            spriteBatch.Begin();

            // オプションを描画
            for (int i = 0; i < optionPositions.Count; i++)
            {
                spriteBatch.Draw(textureKamatoo, optionPositions[i], Color.White);
            }

            // プレイヤーを描画
            spriteBatch.Draw(texturePacchi, playerPosition, Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}


オプションのpositionとvelocityをListにしています。
なるべく手間をかけずにオプションを複数化するために、こうしましたが、普通はこういう場合は、オプションをクラスにして、その中にpositionやvelocityを持たせます。



色んなオプションを作る目次へ戻る



引きずりオプション



引きずりオプションを作ってみましょう。

「引きずりオプション?」と思ったと思います。私が今、勝手に命名したものなので当然です。
まあ、上の画像を見てくれれば、やりたいことはわかってくれると思います。

オプションが自機のうしろについてくるため、一見するとグラディウスのオプションと同じように見えますが、よく見ると違います。一定の距離以上離れたときだけついてくるのです。一定の距離内にいる場合は動きません。引きずられているように見えるので、「引きずりオプション」と名付けました。

正直、2Dシューティングゲームでこういう動きのオプションは記憶に無いですが、ゲーム制作では、結構出てくる動きだと思います。3Dゲームでプレイヤーを追いかけるカメラの動きとかにも使えます。

考え方

下図の親を自機、子をオプションだと思ってください。
青い円は、オプションが引っ張られない範囲です。赤い矢印が半径です。



緑の矢印は、自機とオプションの距離です。
たとえ自機が移動しても、緑の長さ<赤の長さ の場合は、オプションは動きません。
直線の長さを調べるには、有名なピタゴラスの定理が有効ですが、それをやってくれるVector2.Distance()というメソッドがあるので、それを使いましょう。



自機が移動し、オプションが範囲外に出てしまったとします。
つまり 緑の長さ>赤の長さ です。
その場合、オプションを範囲内まで動かす必要があります。
ではどこに動かせば良いでしょうか?





ここです。





ここにオプションを移動させてあげます。
これを毎フレーム繰り返すと、引っ張って引きずっているように見えます。

さて、それでは、上記の位置はどうやって求めれば良いのでしょうか?




まず、上図の緑のベクトルを求めます。自機から見たオプションへの相対位置のベクトルです。これは、オプション位置 - 自機位置 で求まります。





次に、ベクトルを正規化します。ベクトルの正規化とは、難しい響きですが、ただ単に、長さを1にすることです。
ベクトルの各成分(xとy)を、それぞれベクトルの長さで割ってあげれば、長さが1になりますが、それをやってくれるNormalize()というメソッドがあるので、それを使いましょう。
なぜベクトルを正規化するのかというと、長さが1のほうが何かと計算に便利だからです。




最後に、正規化したベクトルに半径の長さをかければ、上図の緑のベクトルとなります。
(このために正規化しました)






サンプルコード

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Trail
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        // プレイヤー移動速度
        const float PlayerSpeed = 5.0f;
        // オプションの距離
        const float OptionDistance = 60f;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // ぱっちぃのテクスチャ
        Texture2D texturePacchi;
        // かまトゥのテクスチャ
        Texture2D textureKamatoo;
        // プレイヤーの位置
        Vector2 playerPosition = new Vector2(200, 200);
        // オプションの位置
        Vector2 option0Position;
        Vector2 option1Position;
        Vector2 option2Position;
        Vector2 option3Position;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像を読み込む
            texturePacchi = Content.Load<Texture2D>("pacchi_32");
            textureKamatoo = Content.Load<Texture2D>("kamatoo_32");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // 上下左右が押されたらプレイヤーを移動させる
            if (Keyboard.GetState().IsKeyDown(Keys.Left))
            {
                playerPosition.X -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
            {
                playerPosition.X += PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Up))
            {
                playerPosition.Y -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Down))
            {
                playerPosition.Y += PlayerSpeed;
            }

            // オプションの位置を更新
            option0Position = CalcOptionPosition(option0Position, playerPosition);
            option1Position = CalcOptionPosition(option1Position, option0Position);
            option2Position = CalcOptionPosition(option2Position, option1Position);
            option3Position = CalcOptionPosition(option3Position, option2Position);

            base.Update(gameTime);
        }

        /// <summary>
        /// オプションのあるべき位置を計算する
        /// </summary>
        /// <param name="currentPosition">オプションの現在の位置</param>
        /// <param name="parentPosition">親の位置(1個目のオプションの親はプレイヤー、2個目のオプションの親は1個目のオプション)</param>
        /// <returns>あるべき位置</returns>
        Vector2 CalcOptionPosition(Vector2 currentPosition, Vector2 parentPosition)
        {
            // オプションと親の距離を算出
            float distance = Vector2.Distance(parentPosition, currentPosition);

            // 距離が一定以下の場合は、移動しない
            if (distance <= OptionDistance)
                return currentPosition;

            // 親から見たオプションへの相対位置のベクトルを算出
            Vector2 direction = currentPosition - parentPosition;
         
            // 正規化(長さを1に)
            direction.Normalize();

            // 親から上限まで離した位置があるべき場所
            return parentPosition + direction * OptionDistance;
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            spriteBatch.Begin();

            // オプションを描画
            spriteBatch.Draw(textureKamatoo, option3Position, Color.White);
            spriteBatch.Draw(textureKamatoo, option2Position, Color.White);
            spriteBatch.Draw(textureKamatoo, option1Position, Color.White);
            spriteBatch.Draw(textureKamatoo, option0Position, Color.White);

            // プレイヤーを描画
            spriteBatch.Draw(texturePacchi, playerPosition, Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}


 



Lerpを組み合わせる

フォーメーションオプションを作ったときにも使ったLerpでヌルっと動かすテクニックは今回も有用です。
下記、黄色部分を変更します。

        Vector2 CalcOptionPosition(Vector2 currentPosition, Vector2 parentPosition)
        {
            // オプションと親の距離を算出
            float distance = Vector2.Distance(parentPosition, currentPosition);

            // 距離が一定以下の場合は、移動しない
            if (distance <= OptionDistance)
                return currentPosition;

            // 親から見たオプションへの相対位置のベクトルを算出
            Vector2 direction = currentPosition - parentPosition;
         
            // 正規化(長さを1に)
            direction.Normalize();

            // 親から上限まで離した位置があるべき場所
            Vector2 desired = parentPosition + direction * OptionDistance;

            // Lerpを使い、あるべき場所に少しずつ近づくように補正してから返却
            return Vector2.Lerp(currentPosition, desired, 0.2f);
        }

毎フレーム、目標地点に20%だけ近づくようにしました。



少し柔らかい動きになりました。ゴム紐っぽいというか。






オプションの数を増やしても面白い

オプションの数をめっちゃ増やしても面白いです。

これは、数を増やして、間隔などを調整した例です。


まるで紐やロープみたいな動きになります。
ぜひ挑戦してみてください。



色んなオプションを作る目次へ戻る




フォーメーションオプション



これはグラディウスIIIにおけるフォーメーションオプションですが、このように、隊列を組んだり、プレイヤーと一定の位置関係を保ったまま移動したりするというのは、ゲームではよくある処理ですね。

基本のプログラム


using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Formation
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        // プレイヤー移動速度
        const float PlayerSpeed = 5.0f;

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // ぱっちぃのテクスチャ
        Texture2D texturePacchi;
        // かまトゥのテクスチャ
        Texture2D textureKamatoo;
        // プレイヤーの位置
        Vector2 playerPosition = new Vector2(200, 200);
        // オプションの位置
        Vector2 option0Position;
        Vector2 option1Position;
        Vector2 option2Position;
        Vector2 option3Position;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像を読み込む
            texturePacchi = Content.Load<Texture2D>("pacchi_32");
            textureKamatoo = Content.Load<Texture2D>("kamatoo_32");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // 上下左右が押されたらプレイヤーを移動させる
            if (Keyboard.GetState().IsKeyDown(Keys.Left))
            {
                playerPosition.X -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
            {
                playerPosition.X += PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Up))
            {
                playerPosition.Y -= PlayerSpeed;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Down))
            {
                playerPosition.Y += PlayerSpeed;
            }

            // オプションの位置を更新する。番号と位置の対応は以下の通り↓
            //
            //          (P)
            //      (0)     (1)
            //  (2)             (3)
            //
            option0Position = playerPosition + new Vector2(-50, 25);
            option1Position = playerPosition + new Vector2(50, 25);
            option2Position = playerPosition + new Vector2(-100, 50);
            option3Position = playerPosition + new Vector2(100, 50);

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            spriteBatch.Begin();

            // オプションを描画
            spriteBatch.Draw(textureKamatoo, option0Position, Color.White);
            spriteBatch.Draw(textureKamatoo, option1Position, Color.White);
            spriteBatch.Draw(textureKamatoo, option2Position, Color.White);
            spriteBatch.Draw(textureKamatoo, option3Position, Color.White);

            // プレイヤーを描画
            spriteBatch.Draw(texturePacchi, playerPosition, Color.White);

            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

何も難しい箇所は無いと思います。
ただ単に、プレイヤーの位置から少しずらした場所にオプションを配置しているだけです。

さて、実行してみると、どうでしょうか?



・・・たしかに隊列は組んでいるのですが、動きが硬いです。
グラディウスIIIのものと比べてみましょう。



グラディウスIIIでは、オプションが少し遅れてヌルっとついてきますね。どちらがカッコいいかは、言うまでもないですね。


ヌルっとついてこさせる

ヌルっと遅れてついてこさせるには、Lerp(ラープ)という定番のテクニックを使います。

LerpというのはLinear Interpolationの略で、日本語にすると線形補間です。
ものすごく難しそうな響きですが、実際は単純で、2つの値を混ぜてくれる関数に過ぎません。2つの値と混ぜる割合を指定すると、結果を返してくれます。

変な例えですみませんが、例えば、母親の頭の良さが100で、父親の頭の良さが200だったとします。
長男は100%父親の血を受け継いだとします。そしたら頭の良さは200ですね。
次男は0%父親の血を受け継いだ(つまり100%母親)とします。そしたら頭の良さは100ですね。
三男は50%父親の血を受け継いだとします。そしたら頭の良さは150ですね。
(実際の遺伝はそんな単純な話ではないですが・・・)
そういう計算をしてくれるのがLerp関数です。

Lerp関数は大抵引数が3つで、上記でいうところの母親の値、父親の値、混ぜる割合を指定します。もちろんこの程度の計算は自力でも簡単にできるでしょうが、せっかく用意されているので、普通はLerp関数を使います。

で、このLerp関数はゲームプログラミングのいたるところで使うのですが、今回のように、ヌルっと動かしたいときにも有用です。

これは、文章で説明されるより、コードと結果を見たほうが良いでしょう。
オプションの位置を更新する処理を、次のように書き換えてください。
            // オプションの位置を更新する。番号と位置の対応は以下の通り↓
            //
            //          (P)
            //      (0)     (1)
            //  (2)             (3)
            //
            option0Position = Vector2.Lerp(option0Position, playerPosition + new Vector2(-50, 25), 0.2f);
            option1Position = Vector2.Lerp(option1Position, playerPosition + new Vector2(50, 25), 0.2f);
            option2Position = Vector2.Lerp(option2Position, playerPosition + new Vector2(-100, 50), 0.1f);
            option3Position = Vector2.Lerp(option3Position, playerPosition + new Vector2(100, 50), 0.1f);

これだけです。たったこれだけで、ヌルっとします。




一体なぜヌルっとしたのでしょうか。

上記のコードでは、オプションの現在位置と、目標位置をLerpで混ぜています。
オプションの0番の処理に注目してみます。Lerpの第1引数に現在位置、第2引数に目標位置、そして第3引数に0.2を指定しています。これは、現在位置から目標位置に20%だけ近づいた場所を表します。
つまり、オプションの0番は、毎フレーム、20%ずつ目標位置に近づきます。
目標位置に近づくほど、移動すべき距離が短くなっていくため、徐々に減速し、ヌルっとした動きになるのです。

オプションの2番ないし3番の処理では、Lerpの第3引数に0.1を指定しています。これは、現在位置から目標位置に10%だけ近づいた場所を表します。つまり、オプションの2番および3番は、毎フレーム10%ずつ目標位置に近づきます。これにより、0番と1番よりも2番3番のほうが、ゆっくり、もっさりとした動きになって味が出ます。



練習問題

以下にチャレンジ!
  • フォーメーションを変えてみる
  • Lerpの第3引数を変えてみる
  • オプションの個数を変えてみる