徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】完成プロジェクトを短時間で改造することになった

 僕の財布にはいつだってお金がない。
 食後に毎回プリンを食べているのが原因のようだ。
 ただお金は無くともゲームは作りたい。
 そんな僕に朗報だ。

【結果発表】山分け方法はみんなで決める!総額 $3,000USD分のアセットストアバウチャー山分けキャンペーン! – Unity公式 Asset Portal

 Unityさんがバウチャーコードをくれるらしい。
 このイベントに参加したらね。

kyotounity.doorkeeper.jp

 なるほど。
 完成プロジェクトのアセットを購入して、そのプロジェクトを改造するゲームジャムですか。
 そして、その完成プロジェクトのアセット代金分のバウチャーコードは頂けると。

 よし参加しよう。
 バウチャーコードのために!

 というわけで今回は完成プロジェクトを改造していた際に「この処理にハマっちまった!」ということがあったお話です。
 自分自身で一から作っているときには、なかなかその作りなどの不便な点には気づかないものですからね。

f:id:urahimono:20180524233533g:plain


この記事にはUnity2018.1.0f2を使用しています。
.Netのバージョン設定には.Net4.x (4.6相当)を使用しています。

変数をシリアライズするということ

 エディタ上でパラメーターを設定したいときは、コンポーネントの変数をシリアライズすればエディタ上で設定できるようになりますよね。

 アクセス修飾子がpublicの変数は、シリアライズされるのでエディタ上に表示されます。
 ただ、エディタ上で設定しなくてもいい変数までエディタ上で表示されてしまうと、情報を設定する人は混乱してしまいます。

f:id:urahimono:20180527074204p:plain

 余計な情報は表示しないようにすれば、混乱を避けられるはずです。
 System.NonSerializedアトリビュートを使うのはどうでしょう。
 これを付けた変数はシリアライズされないので、エディタ上に表示されません。

 アクセス修飾子がprivateの変数は、シリアライズされないのでエディタ上に表示されません。
 でも他のクラスには公開したくないけど、エディタ上ではパラメーターを設定したい。ということは多々あるはず。
 そういう場合は、SerializeFieldアトリビュートを使いましょう。
 これを付けた変数はシリアライズされるので、エディタ上に表示されます。

GameManager.cs

using UnityEngine;

public class GameManager : MonoBehaviour
{
    // エディタ上に表示される。
    public string m_valueA = null;

    // エディタ上に表示されない。
    [System.NonSerialized]
    public string m_valueB = null;

    // エディタ上に表示されない。
    private string m_valueC = null;

    // エディタ上に表示される。
    [SerializeField]
    private string m_valueD = null;

} // class GameManager

f:id:urahimono:20180527074405p:plain

関数がアニメーションから呼ばれることをスクリプトからは読み取れないということ

 これこそが、今回この完成プロジェクトを改造していて。一番ハマっちまったところだ。

 Unityではアニメーションそのものに、イベント関数を仕込むことが出来る。
 アニメーションがアタッチされているコンポーネントの関数を、アニメーションの任意のタイミングで呼び出すことが出来るのだ。
 エディタ上で設定できるので、「アニメーションのこのフレームで攻撃処理を呼ぼう!」ということが簡単にできて便利ではあるんだ。

f:id:urahimono:20180527074745p:plain

 だが、この設定をしていない人にとっては、このやりかたは非常にフローが追いづらいものになってしまう!

 この完成プロジェクトには以下のような処理があったんだ。

Character.cs

private void Shoot()
{
    // 弾を撃つ処理
    // ......
}

 プレイヤーキャラが弾を撃つ処理なんだけど、スクリプトを見る限りどこからも呼ばれていないんだ!
 文字列検索しても引っかからないし、そもそもprivate関数だから、このクラス内からしか呼ばれないはずだし、ボタンのイベントにも設定できないはずだ!

 ここがアニメーションのイベント関数の恐ろしいところだ。
 private関数でも容赦なく呼べるのだ!
 SeneMessage()と同じ方式かよ……。

 このことに気づかなかったり、そもそもアニメーションのイベント関数のことを知らなかったりすれば、ここで詰みます。

 ただ、「アニメーションの任意のタイミングで処理を呼ぶ」ということをやりたいときも出てくると思うので、アニメーションのイベント関数の使用を禁止するのも難しそうだ。

 とすると、スクリプト側で関数名をOnAnimationEvent()などアニメーションから呼ばれることを明示的に示すようにしたり、コメントなどを記述するようにしたりして対応するしかなさそうなんだよなぁ。

いろいろなところでPlayerPrefsを使うということ

 うーん、PlayerPrefsかぁ……。

docs.unity3d.com

 PlayerPrefsをセーブデータとして使うことは、推奨していないという話は聞くよね。
 本格的にセーブデータを作るのならば、Jsonやバイナリ形式にしてファイル読み書きした方がいいだろうさ。
 ただ、このPlayerPrefsを使うのが楽なのは事実だよね。
 だから個人の裁量で使う分に、兎角言うつもりはないのさ。

 ただ問題なのは、いろんなところでPlayerPrefsを呼ぶってことなんだよ。

 この完成プロジェクトでは、ゲーム内課金をする場合の対応も考えてくれているので、武器の数やコインの数などをPlayerPrefsを使ってセーブデータとして保存しているんだよね。

GameManager.cs

private void Purchase()
{
    if( PlayerPrefs.GetInt( "coin" ) > 250 )
    {
        PlayerPrefs.SetInt( "coin", PlayerPrefs.GetInt( "coin" ) - 250 );
        PlayerPrefs.SetInt( "hp", PlayerPrefs.GetInt( "hp" ) + 1 );
    }
}

 だが、いろんなところでPlayerPrefsを呼びまくっているせいで、ゲーム全体として何の情報が保存されているかを把握しづらい。
 何がゲームを立ち上げたら初期化される一時的な情報で、何がゲームをやめても保存されている永続的な情報なのかがわからんのだ!

 せめてセーブデータ関連の処理は、特定のクラスなどにまとめてほしいな。

TimeScaleを0にしてゲームを停止させるということ

 僕はあまり使ったことがなかったんですが、Time.timeScale0にすることで、ゲームの更新を止める方法を使っていました。
 リザルト画面やポーズ画面などを表示するときに使っているみたいですね。
 
GameManager.cs

public void EndGame( int i_index )
{
    switch( i_index )
    {
        case 0:
            Time.timeScale = 0;

            // Pauseメニュー表示処理...
            break;
        case 1:
            Time.timeScale = 1;

            // Pauseメニュー非表示処理...
            break;
        case 2:
            Time.timeScale = 0;

            // GameOverメニュー表示処理...
            break;
        case 3:
            Time.timeScale = 1;

            // 別シーンに移動処理...
            break;

        default:
            break;
    }
}

f:id:urahimono:20180527075424p:plain

docs.unity3d.com

 なるほど、Time.timeScale0にすれば、FixedUpdate()は呼ばれなくなるし、Time.deltaTime0になるみたいですね。
 Update()が呼ばれなくなるわけではないですが、ゲームの更新を止める方法の一つとして利用できそうだ。

 だがこの方法には大きな問題が!
 誰かがTime.timeScaleを1に戻さなくてはいけないということだ。

 この完成プロジェクトのゲームフローをそのまま使う場合は、要所にTime.timeScale1に戻す処理が記述されているだろうから、問題にはならないだろう。

 ただ、このゲームフローに新たなフローを追加する場合に問題が発生しそうだ。
 Time.timeScaleが0のまま、別の処理やシーンに移動してしまう可能性がありえるのだ。
 Time.timeScale0になっていることに気づかなければ、ゲームが正常に動作しなくなってしまう!

 コンポーネントはついているし、Update()も呼ばれているし、エラーも出ていない。
 なのにゲームが動かない!
 この状況は、不具合の原因を突き止めるのになかなか苦労しそうだ!

 Time.deltaTime0にする際には注意したい。

最後だということ

 今回挙げたことは、自分一人でゲームを作っているときには、あまり不便に感じないことばかりなんですよね。
 作った本人は全体を把握しているわけですから、問題になりにくいんですよ。
 ただ、自分自身が作ったプロジェクトでも、時間が経てばどんな処理を書いたか忘れてしまいますからね。
 このあたりのことは気を付けていきたいものです。

 今回の完成プロジェクトを改造することは、いい勉強になりました。

 さて、勉強も終わったことですし、プリンでも食べようかな。