18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

CYBIRDAdvent Calendar 2021

Day 17

Unity1週間ゲームジャムで毎回やるいい加減な実装

Last updated at Posted at 2021-12-16

##はじめに
CYBIRD Advent Calendar 2021 17日目担当の@cy-tatsuya-sakaiです。
16日目は、@kyukkyu81さんのクリスマスの夜にボイパやラップで踊り狂うアレクサでした。
えっ、どういうこと…?

##概要
unityroomで定期的に開催されている1週間ゲームジャム
自分もハマってしまって、こっそり参加してます。
毎回わーって作るだけなんですけど、達成感あるし作品は残るし、色んな人のゲーム遊べるしで嬉しい!有り難いイベントです。

今回は、そんな1週間ゲームジャムでどのように実装を進めているか、自分のやり方を紹介してみようかなと思います!
テクい話というより、毎回やってしまう手癖・いい加減な実装の列挙になります。

作ったゲームはこちら
ちょっとヘンなアクションゲーム多めです。

##①全部FixedUpdate()に書く
可変フレームレート対応とか、時間無くて考えてる暇無い!
とりあえず色んな環境で同じように動いてくれーっ!と、大体の処理をFixedUpdate()に書いてます。

###Fixed Timestepを小さくする
可変フレームレート対応が出来てない場合、オブジェクトの動くスピードはフレームレートに依存します。
FixedUpdate()のデフォルトTimestep 0.02 は、そのままだと少し動きがヌルいです。
PlayerSettingsから適当に0.01と小さくして処理回数を増やします。
コリジョン抜けの強度も上がる気がします。
スクリーンショット 2021-12-08 17.47.36.png

###RigidBody2Dで動かす
RigidBody2Dしか触ったこと無いのでこっちで。RigidBodyもきっと同じ。
FixedUpdate()で直接transformを動かすと、描画とのフレームのズレで動きがカクつく場合があります。
その対応としてRigidBody2Dで動かします。
設定は以下。
スクリーンショット 2021-12-08 17.48.56.png
ポイントは

  • Collision DetectionContinuousに設定
  • InterpolateInterpolateに設定

の2点。

Collision Detectionの設定はおまけです。コリジョン抜けが減ります。
動きに関係してるのはInterpolateの方で、オブジェクトの動きがスムーズになるよう補間してくれます。

BodyType = Kinematicで動かす場合、
RigidBody2D.position、RigidBody2D.MovePosition()どちらで動かしても補間してくれそうでした。
RigidBody2D.positionで動かした場合は、物理エンジン的には物体がワープするような挙動になるみたいなので、もしコリジョンも正確に行いたい場合はMovePosition()で。

処理負荷が増える?大丈夫です、たぶん!

###カメラ座標の更新はLateUpdate()で行う
カメラ座標は他オブジェクトを全て動かし終わった後に更新したいので、LateUpdate()に書くようにしてます。
このとき、他オブジェクトの座標を参照する場合は、Transform.positionを参照します。

RigidBody2DでInterpolateに設定している場合、Transform.positionには補間された座標を入れてくれてるみたいです。
(↑ログ出力してそれっぽい値が入ってたのを確認しただけです。細かいことは知りません…)

###入力について
InputはFixedUpdate()では正しく取得できないので、Update()で入力結果を保持してFixedUpdate()で参照するのをよくやってました。

private bool isInput = false;

void Update()
{
    if(Input.GetKeyDown(KeyCode.Space))
    {
        isInput = true;
    }
}

void FixedUpdate()
{
    if(isInput == false) { return; }
    isInput = false;
    // 入力による処理
}

が前回ゲームを作った際に、上記の書き方だとUpdate()とFixedUpdate()の処理回数の違いによりフレーム単位のシビアな入力が取れない問題が発覚しました。

連打入力が上手く取れなくて、もうダメだ、おしまいだぁと思ったんですけど、新しいInputSystem使ったらサッと解消したのでメモしておきます。
記憶が曖昧ですけどこことかここを参照した気がします。

ポイントはInputSystemの設定で、UpateModeProcess Events In Fixed Updateに変更するだけです。
無題.png

この設定を入れた上で、入力をInputActionから取得したら何だか入力安定しました!
ありがとうUnity先生、まだよく分かってないけど次回もInputSystem使わせていただきます…!

##②ぷにぷにさせる
見た目や手触りをいい感じにしたいのに、ParticleSystemもシェーダーグラフもいざ触ろうとすると忘れてる!時間が無い!
そんなとき、自分は毎回オブジェクトをぷにぷにさせて誤魔化してます。

###イージング
素晴らしい解説があるので見よう!動画もあるぞ。
目標に向かって割合で移動させるこの処理を、ちょっと変更してぷにぷにさせます。

普段↓みたいに書いてるのでそこから改変します。可変フレームレート対応は一旦無視して…
よりイメージ付きやすいように、サンプルはTransform.localScaleを変更する形にしてみました。

private float target; // 適当なターゲット
private float t;      // ターゲットに移動する割合(0.0〜1.0)

var sx = transform.localScale.x;
sx += (target - sx) * t;
transform.localScale = new Vector3(sx, sx, sx);

上記コードでtを適当に設定、targetを1.0と1.2で順に切り替えると以下のような動きになります。
easing001.gif

###速度変数を追加する
すみません、それだけです。
現在値に直接加算せず速度に加算していくと、加速によって目標を行ったり来たりしてぷにぷにします!
単に速度を足すだけだと収束しないので、適当に速度を減衰させます。

private float vel;  // 速度
private float dec;  // 速度の減衰率(0.0〜1.0)。0.5とか0.8とか適当に設定する

var sx = transform.localScale.x;
vel *= dec;
vel += (target - sx) * t;
sx += vel;
transform.localScale = new Vector3(sx, sx, sx);

同じようにtargetを切り替えるとこんな感じ。ぷにっとした感じ分かりますでしょうか?
easing002.gif

速度の方を変更するのもリアクションに使いやすいです。
easing003.gif

###微調整
XとYを別の値にすれば、また違った見た目に出来るかもしれません。
例えばY = 1.0 / Xとかで。

var sy = 1.0f / sx;
transform.localScale = new Vector3(sx, sy, sx);

easing004.gif

振動を抑えたい場合は、targetとの差分で速度を加味すると収束が早くなるかもしれません。

vel += (target - (sx + vel)) * t;

easing005.gif
あとはパラメータを調整していい感じにします。
このパラメータ弄りが楽しいんですよねぇ。中毒性あります。
毎回気づいたら仕込んでしまうので、もう少し我慢して別の表現入れたいところです。勉強しなくては。。

###注意点
もしかしたら気をつけた方が良いかもしれないことを書いておきます。

####①スケーリングはRigidBody2Dで補間されない
RigidBody2Dにはスケールを変更するプロパティやメソッドは無いので、Transformで変更します。
FixedUpdate()でスケーリングすると、若干動きがカクつくかもしれません。
と言いつつ、自分はそこまで気にならないのでFixedUpdate()に書いちゃってますね。。

####②コライダをアタッチしたオブジェクトをスケーリングすると、コライダも変形してしまう
コライダが拡縮を繰り返すことで連続してコリジョン検出されてしまうかもしれません。
コライダの変形を避けるために、アニメーションさせるオブジェクトとコライダオブジェクトを分けた方が良いかもしれません。

##③開始と終了をつける
ゲームの中身を作った。ぷにぷにさせて触り心地を良くした。あとはゲームっぽい流れにすれば遊べるはず!
自分、『レディー』で始まって『クリア』で終わればゲームっぽくなると思い込んでる節があり、
例に漏れず所謂GameController的なクラスにそのような流れを書いています。

###有限ステートマシン
自分はこの部分、有限ステートマシンで作ってます。原始的にはswitch文で管理する感じでしょうか。
こことか参照しました。

private enum State
{
    Ready,
    Main,
    GameOver,
    Clear,
}

private State state = State.Ready;

void FixedUpdate()
{
    switch(state)
    {
        case State.Ready:
            // レディー
        break;
        case State.Main:
            // ゲーム部分
        break;
        case State.GameOver:
            // やられた
        break;
        case State.Clear:
            // ゲームクリア
        break;
    }
}

それでこのswitch文なんですけど、書くのがしんどい…!どうしてもswitch文の中が肥大化してしまいます。
なので簡単なステートマシン用クラスを準備しています。

###自前のステートマシン
今現在はこんな感じのを準備して使ってます。趣味です、書きたかったんです。。。

こんなやつ
using System.Collections;
using UnityEngine;

namespace Lib.Util
{
    public enum StateMachineCase
    {
        Enter,
        Exec,
        Exit
    };

    /// <summary>
    /// 有限ステートマシン
    /// </summary>
    public class StateMachine
    {
        public delegate void State(StateMachineCase c);

        public State Curr { get; private set; }
        public State Prev { get; private set; }
        public State Next { get; private set; }

        public StateMachine(State state)
        {
            ChangeState(state);
        }

        public void Update()
        {
            ChangeNext();
            Curr?.Invoke(StateMachineCase.Exec);
        }

        public void Exit()
        {
            Curr?.Invoke(StateMachineCase.Exit);
            Curr = Prev = Next =null;
        }

        public void ChangeState(State state)
        {
            Next = state;
        }

        private void ChangeNext()
        {
            if(Next == null) { return; }
            if(Next == Curr) { return; }

            Curr?.Invoke(StateMachineCase.Exit);
            Next?.Invoke(StateMachineCase.Enter);
            Prev = Curr;
            Curr = Next;
            Next = null;
        }
    }
}
使い方
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lib.Util;

public class GameMain : MonoBehaviour
{
    private StateMachine _state;
    
    void Awake()
    {
        _state = new StateMachine(State_Ready);
    }

    void FixedUpdate()
    {
        _state.Update();
    }

    /// <summary>
    /// レディー
    /// </summary>
    private void State_Ready(StateMachineCase c)
    {
        switch(c)
        {
            case StateMachineCase.Enter:
            {
            }
            break;
            case StateMachineCase.Exec:
            {
                if(一定時間経った)
                {
                    _state.ChangeState(State_Main);
                }
            }
            break;
            case StateMachineCase.Exit:
            {
            }
            break;
        }
    }

    /// <summary>
    /// ゲーム部分
    /// </summary>
    private void State_Main(StateMachineCase c)
    {
        switch(c)
        {
            case StateMachineCase.Enter:
            {
            }
            break;
            case StateMachineCase.Exec:
            {
                if(クリアした)
                {
                    _state.ChangeState(State_Clear);
                }
                else if(プレイヤーがやられた)
                {
                    _state.ChangeState(State_GameOver);
                }
            }
            break;
            case StateMachineCase.Exit:
            {
            }
            break;
        }
    }

    /// <summary>
    /// ゲームオーバー
    /// </summary>
    private void State_GameOver(StateMachineCase c)
    {
        switch(c)
        {
            case StateMachineCase.Enter:
            {
            }
            break;
            case StateMachineCase.Exec:
            {
                // リトライ処理とか?
            }
            break;
            case StateMachineCase.Exit:
            {
            }
            break;
        }
    }

    /// <summary>
    /// クリア
    /// </summary>
    private void State_Clear(StateMachineCase c)
    {
        switch(c)
        {
            case StateMachineCase.Enter:
            {
            }
            break;
            case StateMachineCase.Exec:
            {
                // タイトルに戻る?
            }
            break;
            case StateMachineCase.Exit:
            {
            }
            break;
        }
    }
}

特徴としては、メソッドがステートになってる点でしょうか。
単に現在のステートメソッドをデリゲートに格納して実行するだけです。

  • ステートに入った際の処理、毎フレームの処理、ステートから抜けた際の処理の3種を分けられる
  • 1フレームに実行されるステートは1つ

の2点だけ抑えて、あとはガバガバです。

メソッドでステートを表現してるのは、ステートを使う側のクラスのメンバにおもむろにアクセスしたかったからです。
多分クラスでステートを表現すると、ステートを使う側のインスタンスをステートクラスが握ってて、
その変数名が仮にoだったとしたらo.privateMenberって書かなきゃいけない。このoを書きたくなかったんですね。
自身のメンバにアクセスするのに毎回this書かないよねって。

僕は一体何の話をしてるんですかね…?

###その他のステートマシン
軽く調べた感じだと、ImtStateMachineが良さそうな気配を感じました。
みんな、車輪の再発明は趣味でやろうな…!
他にはどんなやり方があるんでしょう、謎です。

##④その他
###フォント
大体いつも『フォント フリー』でググって出てきたページを探します。フリーフォント本当に助かります…!
TextMeshProでフォント表示は行っていますが、結構前にDynamicフォントに設定したとき上手く文字が表示されなかった記憶があって(曖昧)、
何か問題出たら面倒臭いと思ってこちらの文字セットを使ってフォントテクスチャ生成してます。

###サウンド
音の再生処理は趣味により自前のSoundManagerで。
こちらも素材は『効果音 フリー』『BGM フリー』で検索してます。
BGMは毎回、甘茶の音楽工房さん使っちゃいます。合わなさそうで合う。有り難いです。
BGM・SE、自作できるようになりたい…!何にも分からん…!

##おわりに
1週間ゲームジャムで毎回やってしまう実装について、雑多に書いてみました。
全体的に時間が無いとか面倒くさいとかばっかりでしたね。。
良い悪いは別にして、何かの参考になれば良いなと思います。

そして次に参加するときはもう少し違った実装が出来るように頑張りたいです…!

明日のCYBIRD Advent Calendar 2021 18日目は、@whitemage_yuさんの「SQLite meets Unity 〜Unityでローカルなデータベースを使おう〜」です。お楽しみに!

18
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?