一週間ゲームジャム「あける」Bubblesを投稿しました

一週間ゲームジャムテーマ「あける」でBubblesというゲームを大遅刻ですが本日公開しました。

あけるのテーマを活かせず、ゲーム自体も非常に短いためあまり遊べる要素はありませんが、今回意識した箇所についてまとめさせていただきます。

動きのあるUI
今までの作品では止まってしまっているUIが多かったため、今回DOTweenを活用してHowtoをスライドインするようにしてみました。(DoozyUIは設定が複雑なのと、DOTweenで表示することに慣れたかったので、今回は不使用です。)

Howtoの表示処理

using UnityEngine;
using DG.Tweening;

namespace Bubbles.Scripts.UI
{
   public class HowtoUICutin : MonoBehaviour
   {

       [SerializeField] private RectTransform panelRectTransform;
       [SerializeField] private RectTransform cutOutpanelRectTransform;
       
       [SerializeField] private float cutInPositionInitX = 1000;
       [SerializeField] private float cutOutPositionTargetX = -1000;

       private void Start()
       {
           DOTween.Init(false, true, LogBehaviour.ErrorsOnly);
       }

       public void PushButton()
       {
           CutOutUI();
           CutInUI();
       }
       
       public void CutInUI()
       {
           if (!panelRectTransform)
           {
               return;
           }
           panelRectTransform.anchoredPosition = new Vector2(cutInPositionInitX,0);
           panelRectTransform.DOLocalMoveX(0f, 1f).SetEase(Ease.OutBounce);
       }

       public void CutOutUI()
       {
           if (!cutOutpanelRectTransform)
           {
               return;
           }
           cutOutpanelRectTransform.DOLocalMoveX(cutOutPositionTargetX, 1f).SetEase(Ease.OutBounce);
       }
   }
}

UniRxによるステータス管理

今までの作品でもUniRxを活用していましたが、今回ステータス管理でも活用することを意識してみました。

プレイヤーのステータス管理

プレイヤーのステータスはプレイヤーのキャッチ状態、泡に触れているなどの状況に合わせて変更するために、キャッチのコンポーネントのキャッチフラグを監視したり、泡に触れている時のコンポーネントの浮遊フラグを監視することでその値が変わったタイミングでステータスを変更しています。

using System;
using Bubbles.Scripts.Manager;
using UnityEngine;
using UniRx;

namespace Bubbles.Scripts.Player
{
   // 状態の種類のenum
   public enum PlayerState
   {
       Idle,
       Dead,
       Carry,
       Floating
   }

   // 独自UniRxステータス
   [SerializeField]
   public class PlayerStateReactiveProperty : ReactiveProperty<PlayerState>
   {
       public PlayerStateReactiveProperty(){}
       public PlayerStateReactiveProperty(PlayerState init) : base(init){}
   }
   
   // インスペクタウィンドウ表示のためのエディタ拡張を定義
//    [UnityEditor.CustomPropertyDrawer(typeof(PlayerStateReactiveProperty))]
//    public class ExtendInspectorDisplayDrawer : InspectorDisplayDrawer{}
   
   // プレイヤー状態
   public class PlayerStatus : MonoBehaviour
   {
       [SerializeField] private PlayerCatch playerCatch;
       [SerializeField] private PlayerFloating playerFloating;
       [SerializeField] private GameObject gameStatusObject;
       public PlayerState currentState
       {
           get { return playerState.Value; }
       }
       
       public PlayerStateReactiveProperty playerState = new PlayerStateReactiveProperty();
       
       private void Start()
       {
           playerCatch = gameObject.GetComponent<PlayerCatch>();
           playerFloating = gameObject.GetComponent<PlayerFloating>();
           
           playerCatch.isCatch
               .Subscribe(_ => PlayerChangeStatus());
           playerFloating.isFloating
               .Subscribe(_ => PlayerChangeStatus());

           if (!gameStatusObject)
           {
               gameStatusObject = GameObject.Find("Managers/GameStatus");
           }

       }

       private void PlayerChangeStatus()
       {
           if (playerState.Value == PlayerState.Dead)
           {
               PlayerDead();
               return;
           }
           
           // プレイヤーが泡に触れている
           if (playerFloating.isFloating.Value == true)
           {
               PlayerFloating();
               return;
           }
           
           // プレイヤーがものを持つ
           if (playerCatch.isCatch.Value == true)
           {
               PlayerCarry();
               return;
           }
           
           // それ以外
           PlayerIdle();

       }
       
       public void PlayerIdle()
       {
           playerState.Value = PlayerState.Idle;
       }

       public void PlayerDead()
       {
           // クリア時は無敵
           var gameStatus = gameStatusObject.GetComponent<GameStatus>();
           if (gameStatus.gameState.Value == GameState.GameClear)
           {
               return;
           }
           
           playerState.Value = PlayerState.Dead;
       }
       
       public void PlayerCarry()
       {
           playerState.Value = PlayerState.Carry;
       }
       
       public void PlayerFloating()
       {
           playerState.Value = PlayerState.Floating;
       }
   }
}

やられ処理については、ダメージを受けた時点でゲームオーバーにするようにしたため、比較的シンプルな作りになります。(本来ダメージ側のスクリプトでステータスを変えるべきではないと思いますが、今回はライフ制ではないので直接ステータスの変更を実施しています。)

using Bubbles.Scripts.Interface;
using UnityEngine;

namespace Bubbles.Scripts.Player
{
   public class PlayerDamage : MonoBehaviour, IDamagable
   {
       [SerializeField] private PlayerStatus playerStatus;

       void Start()
       {
           playerStatus = gameObject.GetComponent<PlayerStatus>();
       }
       public void AddDamage()
       {
           playerStatus.PlayerDead();
       }
   }
}

宝箱のキャッチとリリースについては、UniRxを用いて監視対象とすることで、ステータスを変更させるためにisCatchというフラグを使用しました。このフラグの変更に応じてステータスのUniRxが発火して、ステータスの変更が実施されるようになります。

using Bubbles.Scripts.Interface;
using UnityEngine;
using UniRx;
using UniRx.Triggers;

namespace Bubbles.Scripts.Player
{
   public class PlayerCatch : MonoBehaviour, ICatchable
   {
       [SerializeField] private PlayerStatus playerStatus;
       
       [SerializeField] private IPickable targetTresurebox;
       public ReactiveProperty<bool> isCatch = new ReactiveProperty<bool>(false);
       
       void Start()
       {
           playerStatus = gameObject.GetComponent<PlayerStatus>();
           
           // 宝箱キャッチ
           this.UpdateAsObservable()
               .Where(_ =>
                   (
                       (Input.GetKeyUp("space")) 
                   ) &&
                   playerStatus.playerState.Value == PlayerState.Idle &&
                   isCatch.Value == false &&
                   targetTresurebox != null
               )
               .Subscribe(_ => Catch());
           
           // 宝箱リリース
           this.UpdateAsObservable()
               .Where(_ =>
                   (
                       (Input.GetKeyDown("space")) 
                   ) &&
                   playerStatus.playerState.Value == PlayerState.Carry &&
                   isCatch.Value == true &&
                   targetTresurebox != null
               )
               .Subscribe(_ => Release());
       }
       
       
       void OnTriggerEnter2D(Collider2D other)
       {
           // 宝箱キャッチ
           if (other.gameObject.GetComponent<IPickable>() is var tresurebox && tresurebox != null)
           {
               if (isCatch.Value == true)
               {
                   return;
               }
               
               targetTresurebox = tresurebox;
           }
           
           // 宝箱回収
           if (other.gameObject.GetComponent<IRecoverable>() is var recoverArea && recoverArea != null)
           {
               if (isCatch.Value == false && tresurebox == null)
               {
                   return;
               }
               
//                targetTresurebox.Recovery();
               targetTresurebox = null;
               isCatch.Value = false;
           }
           
       }

       public void Catch()
       {
           targetTresurebox.Behold();
           isCatch.Value = true;
       }

       public void Release()
       {
           targetTresurebox.Normal();
           targetTresurebox = null;
           
           isCatch.Value = false;
       }
   }
}

またプレイヤーのステータスを元にプレイヤーの動きの制御をUniRxを用いて、行なっています。通常時(自由に動ける)、宝箱を持っている時(宝箱が重くて沈んでしまい、横と下にしか進めない)、泡に触れている時(宝箱を持っていても浮かぶことができる)、ゲームオーバーの時(沈む)のように動きの処理を変更するようにしてみました。UniRxのwhereでプレイヤーのステータスを監視することで、ステータスに合わせた処理のみを記載した関数を適用することができたので、動きの処理がバッティングしてしまうことを防いで、各動きの処理にのみ専念する記述ができました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Triggers;

namespace Bubbles.Scripts.Player
{
   public class PlayerMove : MonoBehaviour
   {
       public float moveSpeed; // 現在のスピード

       [SerializeField] private float[] clamp;

       private void Start()
       {
           var playerStatus = this.GetComponent<PlayerStatus>();

           // 通常
           this.UpdateAsObservable()
               .Where(_ =>
                   (
                       (Input.GetAxis("Horizontal") != 0) ||
                       (Input.GetAxis("Vertical") != 0)
                   ) &&
                   playerStatus.playerState.Value == PlayerState.Idle
               )
               .Subscribe(_ => Move());

           // 宝箱運搬
           this.UpdateAsObservable()
               .Where(_ =>
                   (
                       (Input.GetAxis("Horizontal") != 0)
                   ) &&
                   playerStatus.playerState.Value == PlayerState.Carry
               )
               .Subscribe(_ => Carry());

           // 宝箱運搬(自然落下)
           this.UpdateAsObservable()
               .Where(_ =>
                   playerStatus.playerState.Value == PlayerState.Carry
               )
               .Subscribe(_ => Fall());

           // 泡接触&動き
           this.UpdateAsObservable()
               .Where(_ =>
                   (
                       (Input.GetAxis("Horizontal") != 0) ||
                       (Input.GetAxis("Vertical") != 0)
                   ) &&
                   playerStatus.playerState.Value == PlayerState.Floating
               )
               .Subscribe(_ => FloatingMove());
           
           // 泡接触
           this.UpdateAsObservable()
               .Where(_ =>
                   playerStatus.playerState.Value == PlayerState.Floating
               )
               .Subscribe(_ => Floating());

           // やられ
           this.UpdateAsObservable()
               .Where(_ =>
                   playerStatus.playerState.Value == PlayerState.Dead
               )
               .Subscribe(_ => Dead());
       }

       // 通常時の動き
       void Move()
       {
           var x = Input.GetAxis("Horizontal") * moveSpeed;
           var y = Input.GetAxis("Vertical") * moveSpeed;

           if (x != 0 || y != 0)
           {
               var direction = new Vector3(x, y, 0);
               transform.position += new Vector3(x * Time.deltaTime, y * Time.deltaTime, 0);
               Clamp();
           }
       }

       // 運搬時の動き
       void Carry()
       {
           var x = Input.GetAxis("Horizontal") * moveSpeed;

           if (x != 0)
           {
               var direction = new Vector3(x, 0, 0);
               transform.position += new Vector3(x * Time.deltaTime, 0, 0);
               Clamp();
           }
       }

       // 運搬時の動き(自然落下)
       void Fall()
       {
           var y = -1;
           var direction = new Vector3(0, y, 0);
           transform.position += new Vector3(0, y * Time.deltaTime, 0);
           Clamp();
       }

       // 泡接触&動き
       void FloatingMove()
       {
           var x = Input.GetAxis("Horizontal") * moveSpeed;

           if (x != 0)
           {
               var direction = new Vector3(x, 0, 0);
               transform.position += new Vector3(x * Time.deltaTime, 0, 0);
               Clamp();
           }
       }

       // 泡接触
       void Floating()
       {
           var y = +1;

           var direction = new Vector3(0, y, 0);
           transform.position += new Vector3(0, y * Time.deltaTime, 0);
           Clamp();
       }

       // 主人公やられ時の動き
       void Dead()
       {
           var y = -1;
           var direction = new Vector3(0, y, 0);
           transform.position += new Vector3(0, y * Time.deltaTime, 0);
           Clamp();
       }

       // 主人公移動可能範囲への維持
       void Clamp()
       {
           var player_pos_x = Mathf.Clamp(transform.position.x, clamp[0], clamp[1]);
           var player_pos_y = Mathf.Clamp(transform.position.y, clamp[2], clamp[3]);
           transform.position = new Vector3(player_pos_x, player_pos_y, transform.position.z);
       }
   }
}

craftpix.netの2D画像素材
今回2Dのゲームを初めて作ったこともあり、craftpix.netの素材などを活用してみました。かなり安い金額で素材が使い放題になる年間ライセンスがお得だったので、今回会員になって早速活用してみました。

Medlyアプリによる作曲
今回Bubblesで使用した曲はiOSのMedlyというアプリで作曲しています。ループサウンドをドラッグするだけで曲が作れるので、DTMができない私でも簡単に作曲ができます。

以下の曲を使用しています。(こちらで公開している曲はクリエイティブ・コモンズになりますので、ゲームのBGMなどでご活用いただいても大丈夫です。)


Bubblesのソースコード
今回実装したBubblesのソースコードになります。


この記事が気に入ったらサポートをしてみませんか?