慣性オプション

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

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

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

実はグラディウス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を持たせます。



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



0 件のコメント:

コメントを投稿