KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

この記事はKLab Advent Calendar 2017の18日目の記事です

皆さんこんにちは、 jukey17 です

私はクライアントサイドエンジニアとしてゲームのプロトタイプを作る仕事を何度か経験しているのですが、今回はその際に体得した自分なりのノウハウをまとめてみようと思います よろしくお願いします

はじめに

この記事では、Unityを使ったプロトタイプ開発をベースに少人数体制(プログラマ・プランナー・デザイナー各1名ずつなど)で開発を進めていった経験を元に話を進めていきます

なのでUnityを使ったゲーム開発の経験がある方を対象とした内容になっています

プロトタイプ開発の仕事での経験ベースの話ではありますが、個人制作や学校・サークルでのチーム製作にも置き換えて考えられるような内容にまとめているので、参考にして頂ければと思います

仕様を素早く実装する → 仕様の変化を先読みした実装をする

最初に どういった考え方でプロトタイプ開発をすべきか というまとめっぽい話から始めます

大枠としてこれを念頭に置いて開発を進めておきたいということが伝わればと思います

プロトタイプ開発は「どのようなゲームを作っていくべきかを模索する」段階であるため、仕様がキッチリと決まっていることはほぼありません

アレを試してみよう、コレを組み合わせてみよう、やっぱりソレは削ろう... このようにして目まぐるしく実装内容が変わっていくのがプロトタイプ開発です

そうなってくるとついつい「とにかく数をこなすために急いで実装しなければ」と考えてしまうのですが、そうしてしまうと使い回しの効かない書き捨て品質なコードを書いてしまいがちになってしまいます

1回実装して終わりであればその実装の速度を重視して書き捨て品質にコードを書いてもよいと思うのですが、プロトタイプ開発では複数の種類の実装をしたり、その中で幾つかの機能を使い回してみたりと、とにかく色々な要素を組み合わせて面白さを追求していくため1回実装して終わりということは絶対にありません

  • 「先週試して外した機能をもう一度復活させたい」
  • 「パターンAで調整したパラメータをパターンBでも使いたい」
  • 「別の機能で使っている演出をとりあえず使い回してほしい」
  • などなど...

プロトタイプ開発では上記のような試行錯誤が沢山繰り返されます(特に後半になればなるほどこのような内容が増えます)

期間内で継続して開発スピードを出すにはこれらの試行錯誤を如何に予測して捌き続けることができるかが鍵だと個人的には思っています

ということで、上記の例を元にこれらの考え方を持ってどのように開発を進めていくのかを紹介していきたいと思います

プロトタイプ開発でありがちなケースとその対応

「先週試して外した機能をもう一度復活させたい」

仕様の定まっていないプロトタイプ開発ならではのよくあるケースの一つです

これをシューティングゲームにて敵を追尾していく弾を発射するという仕様を例に2つのコードを比較してみましょう

  • サンプルコードA
class Player : MonoBehaviour
{
    [SerializeField] GameObject homingBulletPrefab;
    [SerializeField] float homingBulletProgress;
    [SerializeField] float homingSpeed;
    [SerializeField] GameObject homingFinishEffectPrefab;
    
    GameObject homingBulletObject;

    void Update()
    {
        // 追尾弾発射
        if (Input.GetKeyDown(KeyCode.H))
        {
            // 連続では撃てない
            if (homingBulletObject == null)
            {
                homingBulletObject = Instantiate(homingBulletPrefab);
                homingBulletProgress = 0.0f;
            }
        }

        // 追尾弾の処理
        if (homingBulletObject != null && enemy != null)
        {
            homingBulletObject.transform.position = Vector3.Lerp(transform.position, enemy.transform.position, homingBulletProgress);
            homingBulletProgress += Time.deltaTime * homingSpeed;

            if (homingBulletProgress > 1.0f)
            {
                Destroy(homingBulletObject);
                homingBulletObject = null;

                // 着弾したら自機に演出が出る
                var effect = Instantiate(homingFinishEffectPrefab);
                Destroy(effect, 2.0f);
            }
        }
    }
}
  • サンプルコードB
class Player : ShipBase
{
    // 追尾弾の処理は特になし
}

[ReqiredComponent(typeof(ShipBase))]
class HomingBulletFirer : MonoBehaviour
{
    [SerialzieField] HomingBullet homingBullet;
    [SerializeField] float speed;

    ShipBase parentShip;

    void Start()
    {
        parentShip = gameObject.GetComponent<ShipBase>();
    }

    void Update()
    {
        // 追尾弾発射
        if (Input.GetKeyDown(KeyCode.H))
        {
            // 連続では撃てない
            if (!homingBullet.IsActive)
            {
                homingBullet.Fire(parentShip.transform, parentShip.Target.transform, speed, OnLanded);
            }
        }
    }

    void OnLanded()
    {
        // 着弾したら自機に演出が出る
        EffectManager.SpawnPlayerHomingFinishEffect();
    }
}

class HomingBullet : MonoBehaviour
{
    public void Fire(Tranform start, Transform end, float speed, Action onLanded)
    {
        // 弾を発射する処理
    }
}

サンプルコードAでは追尾弾をそのまま自機(Player)クラスに書いていますが、サンプルコードBでは追尾弾を発射するクラスと追尾弾自身のクラスを分けて実装しています

サンプルコードAの場合は表題の対応をする場合は、該当コードをコメントアウトしておいてそれを解除するか、削除しておいてgitなどのバージョン管理システムから掘り起こして復活させなければなりません

サンプルコードBの場合は HomingBulletFirer クラスを Player クラスが付いている GameObject に付けたり外したりするだけで対応が可能です

今回のようなシンプルな実装内容であれば、サンプルコードAのような実装でも一括でコメントアウトするだけでなんとかなりそうですが、もっと処理が複雑になって色んな所に該当コードが散らばっていくとそう簡単にはいかなくなります

サンプルコードBのように 「機能単位でパーツ(部品)を分けておく」 ことで繰り返される試行錯誤にも素早く対応していくことが可能です

Unityではこのようなまとまった処理をパーツ(部品)として扱うコンポーネント指向を基本とした設計がなされているので、サンプルコードBのようなパーツを分けた実装がしやすい構造になっています

実装を進めながらドンドンとパーツを増やしていき、それらを付けたり・外したり・組み合わせたりするだけで素早く沢山のパターンを試せるような状況を作っていくようにしましょう!

「パターンAで調整したパラメータをパターンBでも使いたい」

プロトタイプ開発では複数パターンを用意して遊び比べることが多くこういった話もよくあるできごとです

こちらもシューティングゲームを題材に2つのサンプルコードでどのように対応すべきかを見ていきましょう

サンプルコードA

// 撃てる弾の種類が複数ある・ボムが使える自機
class PlayerA : MonoBehaviour
{
    [SerializeField] int baseHp;
    [SerializeField] int baseAttack;
    [SerializeField] float moveSpeed;
    [SerializeField] BulletType[] usableBulletTypes;
    [SerializeField] int maxBombCount;

    ...
}

// 撃てる弾は1種類で特殊なスキルが発動できる自機
class PlayerB : MonoBehaviour
{
    [SerializeField] int baseHp;
    [SerializeField] int baseAttack;
    [SerializeField] float moveSpeed;
    [SerializeField] float skillIntervalTime;

    ...
}

サンプルコードB

class PlayerParam : ScriptableObejct
{
    [SerializeField] int baseHp;
    [SerializeField] int baseAttack;
    [SerializeField] float moveSpeed;
}

// 撃てる弾の種類が複数ある・ボムが使える自機
class PlayerA : MonoBehaviour
{
    public void Initialize(PlayerParam initialParam, BulletType[] usableBulletTypes, int maxBombCount)
    {
        // パラメータの保持などの初期化処理
    }

    ...
}

// 撃てる弾は1種類で特殊なスキルが発動できる自機
class PlayerB : MonoBehaviour
{
    public void Initialize(PlayerParam initialParam, float skillIntervalTime)
    {
        // パラメータの保持などの初期化処理
    }

    ...
}

サンプルコードAでは2パターンの自機をクラスに必要なパラメータを SerializeField 属性を付けて定義していますが、サンプルコードBではパラメータクラスを用意して初期化処理を呼んだ際にそれを設定しています

どちらのコードも外からパラメータを設定できるようにしている点は同じですが、サンプルコードBではパラメータを専用のクラスに逃している点に注目してください

このようにロジック(自機の挙動を定義する Player) とデータ(自機のパラメータを定義する PlayerParam)に分けることでパラメータの使い回しが容易になります

今回は自機を対象としていますが、シューティングゲームであれば、敵や弾、ボムなどある程度の機能単位でデータを別クラスに逃しておくのをオススメします

このようにして先にデータを切り出しておくことで、ある程度内容が固まったあとにプランナーの人を中心にパラメータ調整を行うといったことになってもスムーズに移行することができます

Unityの場合、データとして扱うために最適化された ScriptableObject やJSONを扱うための JsonUtility などがあるのでそれを活用しましょう!

Tips パラメータを外部から設定できるようにするためには何を使うべきか?

SerializeField 属性(Inspector)を使う

一番手っ取り早いのはサンプルコードAでも紹介している SerializeField 属性を使ってInspectorから設定できるようにする対応です

これは複数パターンを試したり色んなところで使い回したりしないようなパラメータ(ゲーム全体の設定など)を弄れるようにしたい場合に使いましょう

ScriptableObject を使う

SerializeField を使う場合に比べて実装コストは少し上がりますが、サンプルコードBのように複数パターンを試したり色んなところで使い回したりするような場合に効果を発揮できます

一度構造を作ってしまえばデータを複製してパターンも作れますし、そのままAssetBundle化することも可能です

のちのちエンジニアの手を使わずにデータのチューニングをしていくことを見越しておくと先にデータを外部ファイルに逃がしておいたほうがよいです

JSONを使う

利点は ScriptableObject と同じです

最終的にそのデータをどのように取得してくるかを想定して使い分けるとよいと思います

APIを叩いてレスポンスを受け取ってから次の処理に進むようなケースが想定されている場合はデータをJSONで定義しておいたほうが都合が良いでしょう(ステージの難易度情報やデッキの情報など)

「別の機能で使っている演出をとりあえず使い回してほしい」

仮で用意しておき、後から違うものに入れ替えるという状況もプロトタイプ開発ではよくあるケースです

プロトタイプ開発ではゲームの中身をどれだけ面白くできるかに注力して進めていくので、見た目の作り込みが後追いになることが多いです

進め方によってはデザイナーは関与せずにプリミティブなオブジェクト(簡単な記号や図形)のみを組み合わせただけでプロトタイプ開発を行うこともあります

そういった場合でも手戻り少なく実装を進めるにはどうするべきか、こちらもコードを見てみましょう

サンプルコードA

class Player : MonoBehaviour
{
    // HPを回復したときに呼ばれる
    void OnHeal()
    {
        // 回復演出は出しっぱなしにしてそのまま消す
        var effect = Instantiate(healEffectPrefab, transform);
        Destroy(effect, 1.5f);
    }

    // HPがゼロになったときに呼ばれる
    void OnDead()
    {
        if (isDead)
        {
            return;
        }
        StartCoroutine(PlayDeadEffectCoroutine);
    }

    IEnumerator PlayDeadEffectCoroutine()
    {
        var effect = Instantiate(deadEffectPrefab, transform);
        yield return new WaitForSecond(2.0f);

        Destroy(effect);
        // ゲームオーバー処理
        GameOver();
    }
}

サンプルコードB

class Player : MonoBehaviour
{
    // HPがゼロになって死亡演出が完了した後に呼ばれる
    public Action OnFinishDeadEffect { get; set; }

    // HPを回復したときに呼ばれる
    void OnHeal()
    {
        EffectManager.EmitHealEffect();
    }

    // HPがゼロになったときに呼ばれる
    void OnDead()
    {
        EffectManager.EmitDeadEffect(OnFinishDeadEffect);
    }
}

サンプルコードAでは Player クラスに直接エフェクトの発生処理を書いているのに対し、サンプルコードBでは エフェクトの発生処理を EffectManager にお願いする形になっています

自機のための演出になるので Player クラスに処理を書くと言う考え方は間違ってはいませんが、ゲームでの演出処理は「演出が終わったら」や「途中で中断したい」などの複雑な対応をするケースが多いです(そして調整の度にそのタイミングが代わります)

Prefabなどにまとめて全て演出側に処理を逃がせれば理想的ですが、内容がコロコロ入れ替わるプロト開発ではいきなり全ての処理を外に逃がすのも難しいです

そこでサンプルコードBでは EffectManager なる演出の複雑な遷移などをアレコレ吸収してくれる機能を外に用意しました

このようにしておくことで演出の差し替えが後で入ったとしても基本的には EffectManager に手を入れるだけにすることができます

ただし EffectManager をそのままエフェクトを何でも処理してくれる便利屋として扱っていくと今度は EffectManager が肥大化していって整理が難しくなってきてしまうので、その場合は様子を見つつ EffectManager も分割していきましょう

まとめ

ということでここまでつらつらと書き連ねて来ましたが、内容的にはプログラミングを設計する上で気をつけた方がよい基本的な事柄をまとめているということに気づきましたでしょうか

要は プロトタイプ開発であろうと速度重視の書き捨てコードを書くべきではない というのが私の結論です

少人数(1人)で開発を進めていくとついついルーズにコードを書いてしまいがちですが、開発が進んでいけば自分以外の人も多く関わってくることを考えると常に整理しながら開発を進めていけるのが理想です

個人の開発だったとしても、何日か経ったあと自分の書いたコードを見返して何が書いてあるのかよく分からなくなると言う経験はしたことがあるはずです

未来のチームメンバー(自分)が少しでもラクにできるコードを書くように心がけていきましょう!

明日の19日目はsuzu-jさんです。よろしくお願いします。

はじめに

この記事は KLab Advent Calendar 2017 15日目の記事です。
こんにちは。S-Typeと申します。普段はプランナーをしている企画の人間ですが、何か書いてみたいと言ってみたところ参加させてもらえることになりました。
Google Home miniから勤怠メールを送る方法をまとめます。

勤怠報告は大変

季節の変わり目は体調を崩しがちです。無理してもパフォーマンスは上がりませんし、そういうときは休みましょう。
しかし、重い体を動かしながら、一生懸命勤怠メールを打とうにも誤字脱字が多発し、修正に時間を取られてしまう……なんてことはありませんか。

byouki_oldman

そこで、Google Home miniの出番です。
Google Home miniに勤怠を送ってほしいと、寝たまま声を発するだけで送ってくれる仕組みをご紹介します。

Google Home miniとは

Google Home miniは、Google アシスタントが利用可能なスマートスピーカーであるGoogle Homeの小型版です。価格も約半額の6,000円で購入することが可能です。
「OK, Google」もしくは「ねえ、Google」から会話を開始し、以降に続けた音声コマンドを実行してくれます。例えば「OK, Google. 明日の天気は?」と聞くと「明日の〜は最高気温〜、最低気温〜で晴れるでしょう」と回答してくれます。
その他にも計算式、radikoやNHKニュースの再生、タイマーや目覚ましアラームなど、日常においての頼れるパートナーとしての役目を担ってくれます。

IFTTTとは

IFTTTは、 If This Then That をコンセプトに生まれたウェブサービスです。
トリガーである【This】とアクションである【That】、それぞれ別々のサービスを設定し、連携させることができます。
IFTTTで特筆すべきはトリガーとアクションそれぞれに設定可能なウェブサービスが豊富にあることと、こうした流れを コードを書くことなく ユーザーが任意に設定できるということです。
そして、こうした設定をレシピと呼び、公開すれば多数のユーザー間で共有することが可能です。
今回は、このIFTTTのトリガー部分にGoogle Homeで利用可能なGoogleアシスタントを設定し、Gmailから勤怠メールを送ってみることを試してみようと思います。

IFTTTレシピ作成前の準備

では、さっそく、レシピを作ってみましょう。準備するものは以下の通りです。

  • Googleアカウント(Gmailで送る際に使用)
    • IFTTTに登録可能なGmailアカウントは一つだけです
  • IFTTTのアカウント

そして、処理の流れは以下の通りとなります。

  • This:Google Home miniに「今日は会社を休みたい」と言う。
  • That:Gmailから勤怠メールが送られる。Google Home miniから「お大事にして下さい」と返される。

レシピの追加

  1. IFTTTにログインし、My AppletからNew Appletをクリックします。

recipe_0

  1. この画面が表示されるのでThisをクリックします。

recipe_1

  1. Google Assistantをクリックします。探しにくい場合は検索で見つけましょう。

recipe_2

  1. Say a simple phraseをクリックします。
    これは「今日は会社を休みたい」という単一のフレーズでの処理結果が確定しているためです。
    今回は解説しませんが、今日ではなく明日だったり、休む理由を体調不良や私用など条件分岐させたい場合はSay a phrase with a text ingredientを選択します。

recipe_3

  1. Google Homeに話しかけるときのフレーズを指定します。
    対象の項目は
    What do you want to say?
    What's another way to say it? (optional)
    And Another way? (optional)
    以上の3つです。
    What do you want the Assistant to say in response? はどんな返事をしてほしいかを設定します。
    日本語で指示し、日本語で返してほしいので Language はJapaneseにしてください。
    ここまで終わったらCreate triggerをクリックしてトリガーの作成を終わらせましょう。

recipe_4

  1. 次にThatをクリックし、トリガーをきっかけに実行される内容を設定しましょう。

recipe_5

  1. Gmail をクリックします。

recipe_6

  1. メールを送りたいので、Send an emailをクリックします。

recipe_7

  1. メール送信にあたって必要な情報を埋めていきます。
    ここで設定しているのは以下の項目です。
    To address で送信先を、Subject で件名を、そして Body で本文を設定します。
    Bodyに改行を含む場合は <pre> タグで文章を全て囲わないと、改行されませんので注意してください。
    ここまで終わったらCreate actionをクリックしてアクションの作成を終わらせましょう。

recipe_8

  1. 最後にレシピの名前を設定します。デフォルトでIfから始まる文章が入っていますので、このままでも良いでしょう。
    Receive notifications when this Applet runs のスイッチはOFFにしておくことをオススメします。
    このスイッチがONになっていると、そのレシピが実行されたらその度にメールで通知されます。
    Finishで完了します。

recipe_9

検証

では、さっそく検証してみましょう。

前提条件

  • メールの送り元は私用のアドレスです
  • メールの送り先は社用のアドレスです

実践

私「ねえ、Google。今日は会社を休みたい」
Google Home mini「お大事にして下さい」
……しばらくしてから ピコン!(メールの着信音)

スクリーンショット

出来ましたね!!

recipe_10

最後に

今回はGoogle Home miniとIFTTTの連携によるメールの送信を紹介しました。
100%衝動買いだったGoogle Home miniでしたが、IFTTTのおかげで様々なウェブサービスとの連携が楽しめるので、無駄にはならなさそうです。
何よりもコードを書く必要がなく、GUIで入力項目を埋めていきながら設定が出来るため、私のような開発職以外の人間でも簡単に出来ました。

余談

Google Home miniを今後も活用していきたいので、nature remoを注文しました。
nature remoと組み合わせることで、音声による赤外線リモコン家電のコントロールが可能になります。
これが出来るようになると、寝たまま部屋のリモコン照明のON/OFFが出来るようになります。もう完全に未来ですね。
本稿を書くまでに届いていれば、そちらをテーマに書こうと思っていたぐらいでしたが、間に合わなかったため、またいつか別の機会で書かせてもらえたらと思っています。

初めに

この記事は KLab Advent Calendar 14日目の記事です。

こんにちは、クライアントエンジニアの norm81 です。

初登場です、よろしくお願いします。  

経緯  

Unityでナビメッシュのベイク情報をプレハブにしたい、そしてアセットバンドルで運用したいという要望がありました。

既存のA*アセットなどを検証していたところ、Unity5.6からNavMeshBuilderが強化されたのでそちらで実現可能か検証しました。 

静的にベイクしたデータを用いたサイトをあまり見掛けなかったので今回纏めてみようと思います。  

検証に用いたUnityバージョンは5.6.3f1です。

結論

いきなり結論になりますが、上記の要望は対応できました。  

但し、制限があります。  

今回の方法において注意点も見つかりましたので、併せて下記に記載しています。  

導入

下記をクローンし、対象のプロジェクトに入れます。

https://github.com/Unity-Technologies/NavMeshComponents/

Assets/NavMeshComponents/フォルダをコピーしてねと書かれてますが、  

今回はAssets/Examples/フォルダも用います。  

手順

基本的には既存にあるナビメッシュの作成の通り、設定を行なっていきます。  

但し、Bakeは別途行うのでこのタイミングでは行いません。  

  

ヒエラルキーでナビメッシュの対象にするRendererコンポーネントを持つオブジェクトの親に、一つ空オブジェクトを作成します。  

そしてAssets/Examples/フォルダに入っているNavMeshPrefabInstanceコンポーネントをAddComponentします。  

01

そのオブジェクトを含むプレハブを保存した後にインスペクタのSelectボタンを押す、或いはプロジェクトウィンドウから保存したプレハブを選択すると、入れ子にあるオブジェクトが表示されます。

そのままNavMeshPrefabInstanceコンポーネントを持つオブジェクトを選択して、インスペクタのNavMeshPrefabInstanceコンポーネントのBakeボタンを押します。  

するとプレハブにNavMeshが追加されます。

確認 

Navigationウィンドウを開き、Show NavMeshを有効にした状態でWalkableのメッシュが表示されていれば成功です。

02

表示されていない場合、追加されたNavMeshを確認してみてください。Source BoundsのExtents値が(0, 0, 0)だと失敗している可能性が高いです。

03

下記の注意点を確認いただき、再度上記のBakeを試みてください。

成功した後は、このプレハブをアセットバンドルにすればそのまま運用可能です。

具体的な挙動については、上記NavMeshComponentsのソースコードを参考されることをオススメします。

注意点

NotWalkableを反映したナビメッシュがBakeできない。

従来のNavigationウィンドウでのBakeとは異なり、差集合のベイクがなされないです。  

対案としてMeshColliderを併用して目的座標に対して上空からのRayCastで判定を行い進入可能か調べる様なような二段構えの対応を検討しています。

FBXなどModelImporterを介するMeshを扱う場合、Bake前にRead/Write Enableを有効にする必要がある。  

04

プレハブに保存した後に無効に戻すのは問題なかったです。  

まとめ

現時点の外部データ化するメリットは、

  • ランタイムでベイクしなくて良いので処理コストが軽減できる。  
  • 1つの背景データと複数の外部データ化したナビメッシュを組み合わせることで、クエスト進行度などに併せてナビメッシュを切り替える運用ができる。  

といったところで、デメリットは注意点で記載したとおりになります。  

今回紹介した機能は折角の標準搭載された機能なので使い倒してみたいと思います。

以上

↑このページのトップヘ