simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:高画質スクリーンショット

ゲーム画面を高画質で記録したい

次のスクリプトをシーンに追加して、InputAction で KeyBoard > PrintScreen を指定したらできました。

PrintScreenshot.cs

using UnityEngine;
using UnityEngine.Experimental.Input;

public class PrintScreenshot : MonoBehaviour
{
    public InputAction inputAction;

    void Awake()
    {
        inputAction.performed += ctx =>
        {
            ScreenCapture.CaptureScreenshot(System.IO.Path.Combine(Application.dataPath, "../ScreenShots/" + System.DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".png"), 4);
        };
    }

    public void OnEnable()
    {
        inputAction.Enable();
    }

    public void OnDisable()
    {
        inputAction.Disable();
    }
}

参考記事

tsubakit1.hateblo.jp

Unity:ゲームの土台プロジェクト

前書き

Unity ゲームエンジンそのものが土台といえば土台かもしれませんが、Unity でのゲーム作りの土台となると
おおよそ形は決まってくるもので、ゲームの面白さのアイディアなどはその土台の上にのせるだけ

よく上司から勧められるゲームジャムのイベントに「行こう!」という気になれないのは
この土台を作れる or いつでも引き出しから出せる状態にないからと言いたいのですが
ならば、すぐに土台を用意できるように用意しようと思った次第

これまで書いてきた記事を参照しながら、土台の作り方を詳細に記録します。

目次

最初にやるのはバージョン管理

プログラムは資産ですから、長く使うことになるだろうプロジェクトのバージョン管理は必須だと思います。
GitHub を使うのはユーザーが多くて無料という単純な理由です…

simplestar-tech.hatenablog.com

この記事には紹介は無いですが、最近は Source Tree による直感的なソース管理に切り替えています。
新規で Unity プロジェクトを作ったら、まずは空プロジェクトの状態で公開します。
README でどのようなプロジェクトにするか明記して、あとはその目標に足りていない部分を見つけて足していきましょう。

Unity の基礎実装

Unity はエディタ操作でゲーム作りのかなりの領域をカバーしています。マニュアルの各コンポーネントの概要にざっと目を通せばそれに気づけますが…
docs.unity3d.com

これを全部読み終えて感じることは、ゲーム作りの本質はC#プログラミングにあるという一点だけです。
上のマニュアルで紹介されたコンポーネントは、ブロックでいうところの「洗練されたカッコイイ部品」といった役割を果たし
それぞれを縫い合わせる糸、接続のための粘土ともいえるプログラムは、私たちが創らなければなりません。

いきなり極寒の地に裸で放り出されるわけですが、そこには基本的な縫い合わせ方や、粘土をこねる順番や型というものがあるので、そのうちの一つを今回紹介したいと思います。

成果物はこちらに公開していきます。
github.com

プロジェクトのフォルダ構成

Unity 使う上では超重要、自分のスタイルはこちら
Unity初心者向けアドバイス - simplestarの技術ブログ

ちゃんと考えたい人はこちらが参考になるかも
r-ngtm.hatenablog.com

チームロゴの表示

これ Splash Screen と言って Player Settings のこちらで設定できます。

f:id:simplestar_tech:20190106112616j:plain
Splash Screen 設定
今回適当なロゴを使ってみました。
f:id:simplestar_tech:20190106144836g:plain
スプラッシュスクリーンを自作のロゴに変更した結果
ここはプログラミング関係なかったね…

タイトル画面

上記動画にも映っている Press Space Key が点滅表示されているだけのシーン
uGUI と Animation 使うだけです。

忘れちゃった時のために、ウィンドウの出し方だけ記録しますね。

  • Window > Animation > Animation メニューをクリック
  • Create Animation をして、Properties に Text Mesh Pro Text の Font Color を指定し、キーフレームを移動してから Font Color を変更、Add KeyFrame ボタンを押す
  • 作られた .anim ファイルを Text に追加した Animation コンポーネントに設置する

タイトルメニューの表示

Space キーを押すとアクティブなパネルが切り替わる実装について記事を書きました。
simplestar-tech.hatenablog.com

パネルにて GridLayoutGroup コンポーネントを利用すると、自動的にボタンが整列して配置されるので、システマチックで整った UI を見ることができます。

f:id:simplestar_tech:20190106180144g:plain
タイトルメニューの表示とボタンイベント処理

ボタンを押すとゲームが終了するのは、ボタンに次の記事で示すスクリプトを割り当てているからです。
simplestar-tech.hatenablog.com

New Game への移行とローディング画面

少し前に作った VRM を初期フレームで読み込むシーンを NewGame としましょう。
その時のローディング中の画面表示方法について示します。

といっても以下のページを参考にしました。
gametukurikata.com

加工した Slider に次の LoadingSlider.cs をアタッチするだけです。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Slider))]
public class LoadingSlider : MonoBehaviour
{
    internal IEnumerator LoadCoroutine(AsyncOperation async)
    {
        while (!async.isDone)
        {
            var progressVal = Mathf.Clamp01(async.progress * 1.1f);
            if (_slider == null)
            {
                _slider = GetComponent<Slider>();
            }
            _slider.value = progressVal;
            yield return null;
        }
    }

    Slider _slider;
}

GameManager

Script 名を GameManager にするとアイコンがギアに変わります。
GameManager はゲーム内に一つだけしか存在しませんので、それを強制する C# プログラミングテクニック「シングルトン」を使います。

いつかの記事を発見、こちらを参考に GameManager クラスを作ります。
Unity:シーンに一つだけしかアタッチできないSingletonMonoBehaviour+実装例 - simplestarの技術ブログ

ファイル選択による VRM のロード

こちらのアセットを導入して、ファイル選択結果で得たパスを使ってロードするように書き換えます。
assetstore.unity.com

InputSystem の InputActions によるカメラカメラ操作

次の記事の通り
simplestar-tech.hatenablog.com

これからの Unity は新しい InputSystem を使うことになります。

高画質でスクリーンショット

機能を付けておきました。
simplestar-tech.hatenablog.com

動的地形生成

簡易的に次のコードで終わらせた。
TimeUpdate はなかなか便利

LevelPlaneGenerator.cs

using System.Collections;
using UnityEngine;

public class LevelPlaneGenerator : MonoBehaviour
{
    [SerializeField]
    GameObject levelPlanePrefab;

    void Start()
    {
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        yield return new WaitForSeconds(4.0f);
        while (true)
        {
            Instantiate(levelPlanePrefab, new Vector3(_levelOffset += 2, _levelOffset / 5.0f, _levelOffset), Quaternion.identity, transform);
            yield return new WaitForSeconds(4.0f);
        }
    }

    int _levelOffset = 0;
}

土台プロジェクト完成

この記事では、次のツィートのようなゲームの土台の作り方を詳しく示しました。

去年からずっとゲームの面白さのコアな部分を作る方法を調べていたのですが
今回はフレーム的な、どんなゲームにも含まれる要素を作ってみました。

今後はこれを土台に、メインのゲーム要素を色々と載せ替えながら作業を記録していこうと思います。

Unity:InputSystemのInputActionsによるカメラ操作

ゲームパッドの左スティックでカメラの移動
右スティックでカメラのパン・チルト調整を行うように実装してみました。

次のクラスを Camera にアタッチすると機能します。

TPSCameraBehaviour.cs

using UnityEngine;
using UnityEngine.Experimental.Input;

[RequireComponent(typeof(Camera))]
public class TPSCameraBehaviour : MonoBehaviour, ICameraActions
{
    [SerializeField]
    TPSCameraInput cameraInput;

    void Awake()
    {
        cameraInput.Camera.SetCallbacks(this);
    }

    void Start()
    {
        _camera = GetComponent<Camera>();
    }

    void Update()
    {
        var nextEuler = _camera.transform.eulerAngles;
        nextEuler += _cameraRotateEuler;
        _camera.transform.eulerAngles = nextEuler;
        _camera.transform.LookAt(_camera.transform.forward + _camera.transform.position);
        _camera.transform.position += _cameraVelocity;
    }

    void OnEnable()
    {
        cameraInput.Enable();
    }

    void OnDisable()
    {
        cameraInput.Disable();
    }

    public void OnLeftStick(InputAction.CallbackContext context)
    {
        var value = context.ReadValue<Vector2>() * 0.1f;
        _cameraVelocity = _camera.transform.right * value.x;
        _cameraVelocity += _camera.transform.forward * value.y;
    }

    public void OnRightStick(InputAction.CallbackContext context)
    {
        var value = context.ReadValue<Vector2>();
        _cameraRotateEuler.y = value.x;
        _cameraRotateEuler.x = -value.y;
        _cameraRotateEuler.z = 0;
    }

    public void OnLeftClick(InputAction.CallbackContext context)
    {
        Ray ray = _camera.ScreenPointToRay(_mousePosition);
        // GameManager.Instance.SetAgentDestination(ray);
    }

    public void OnMousePosition(InputAction.CallbackContext context)
    {
        _mousePosition = context.ReadValue<Vector2>();
    }

    Camera _camera;
    Vector3 _cameraVelocity = Vector3.zero;
    Vector3 _cameraRotateEuler = Vector3.zero;
    Vector2 _mousePosition = Vector2.zero;
}

上記で登場する TPSCameraInput ・ ICameraActions クラスは自動生成されたものです。
Assets > Create > InputActions で TPSCameraInput という名前で InputActions を作成すると作られます。
内部の ActionMaps に Camera を追加すると ICameraActions クラスが作られます。

f:id:simplestar_tech:20190112185357j:plain
Create>InputActions で TPSCameraInput を作成・編集した様子

参考にした記事

tsubakit1.hateblo.jp

Unity:uGUIボタンとゲームロジックの縫い合わせ方法

前書き

ユーザーによる画面入力とゲームのロジックが接続される箇所を探すと、ボタンが大部分を占めます。
ボタンを押した時にどのプログラムが起動するかを UI オブジェクトから漏れなく、間違いなく辿れなければなりません
(※個人的な意見です)

が、そうしないプログラムが多く、実際メンテナーに大きなプロジェクトを引き継いだ時
ボタンを押したらバトルが始まるのに、そのボタンを押した時に実行されるプログラムがコードから見つけられない、処理フローを限定できないという大問題が発生します。

これを避ける良いプログラミング例が無いかなぁと考えて、自分は以下のようにしています。

QuitGameButton に割り当てるスクリプト実装

QuitGameButton.cs

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Button))]
public class QuitGameButton : MonoBehaviour
{
    void Start()
    {
        _button = GetComponent<Button>();
        _button.onClick.AddListener(OnClick);
    }

    private void OnClick()
    {
        Application.Quit();
#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;
#endif
    }

    Button _button;
}

Unity のチュートリアルだとインスペクタで呼び出す関数を onClick イベントに登録しますが、それだとインスペクタの設定項目とコード実装接続を人間が行わなければならず
ヒューマンエラーが起きやすく、作業者の集中リソースを消費して会社の労働力が減ってしまいます。

そこで、上記の通り、Start 関数内で AddListner 関数を用いてコード側でボタンとロジックを接続するようにしました。
このルールが徹底されていれば、押下するボタンに割当たっているスクリプトを調べれば、漏れなく処理フローを、間違えることなく把握できるようになります。

(このルールが徹底されていれば…問題はちょっと実装が面倒くさい点ですね、多くの人はインスペクタからイベントに関数を登録してしまいます…)

Unity:InputSystemのInputActionを使ってみた

前書き

まだ半年早いですが、これから作るゲームは新しい InputSystem で作っていきたいと思います。
ドキュメント読んで動作確認までできたので実装コードを記録しておきます。

以下、自問自答した内容です。

Input System の InputAction でキー押下イベントを利用

「スペースキーを押したらゲームオブジェクトの表示非表示を切り替えるコード実装見せて」

「以下の通りです」

PressSpaceKeyText.cs

using UnityEngine;
using UnityEngine.Experimental.Input;

public class PressSpaceKeyText : MonoBehaviour
{
    [SerializeField]
    GameObject menuPanel;
    [SerializeField]
    GameObject titlePanel;

    public InputAction inputAction;
    
    void Awake()
    {
        inputAction.performed += ctx => {
            menuPanel.SetActive(true);
            titlePanel.SetActive(false);
        };
    }

    public void OnEnable()
    {
        inputAction.Enable();
    }

    public void OnDisable()
    {
        inputAction.Disable();
    }
}

「なるほど、インスペクタの方で Space Key を指定する感じですね?
インスペクタビューを見せてください」

「こちらです」

f:id:simplestar_tech:20190106163115j:plain
上記スクリプトをアタッチしたオブジェクトのインスペクタ

「新しい Input System の導入方法は?」

※この記事を書いて数日後に Package Manager 経由でインストールできるようになりました

「以下の Git を Clone して、Packages フォルダの com.unity.inputsystem をコピーして Editor を再起動します。」
github.com

「OK、細かいところは Git のページを見よう」

おわりに

こういう、欲しい情報をピンポイントで答えてくれる AI ほしいなぁ

Unity:動的NavMeshの確認

ゲームの3大AI(人工知能)の一つはナビゲーションAI

ゲームAIを大きく分類すると次の三つがあることに気づけます

  • キャラクターAI
    • キャラの振る舞いを決定
  • ナビゲーションAI
    • 目的地への経路を決定
  • メタAI
    • ゲームの進行を決定

今回強く関係しているのはナビゲーションAIです。

キャラクターが行動できる範囲を Walkable (歩行可能) エリアと呼びますが、次の動画の水色の半透明な領域が計算によって求めた Walkable エリアです。

f:id:simplestar_tech:20190105172931g:plain
動的NavMeshの動作の様子
f:id:simplestar_tech:20190105173305g:plain
動的NavMeshの動作の様子2

ゲームプレイ中に動的に Walkable エリアが更新されていることが確認できました!
これ本当にすごいことを目の当たりにしている瞬間でして、長いこと Unity では静的にしか Walkable エリアを計算できませんでした。
要するに、ゲーム実行前のビルドの段階で Walkable エリアを作り込むことしか選択肢がなかったのです。

計算が非常に複雑な経路計算がゲーム実行中にできる
それは単にナビゲーションが動的に作成した地形に適用できるという話では収まりません
あらゆる知的計算は経路探索に置き換えることができます。
例えば、今日この記事を書いている私の頭の中は、様々な行動プランを選択できる状態の中、この記事を書き始めるべきだと経路を作って、その経路に従って行動しています。
つまり、キャラクターAIのプランニング機能を動的 NavMesh 生成処理で実現できます!
(そんな気づきを得て興奮できる人がいるかは別として…)

Unity での具体的な実装方法

まずは次の Unity マニュアルを最後まで読む
docs.unity3d.com

実装手順だけ簡単に示します。
ドキュメントに書いてある通りこの動的 NavMesh 機能は標準の Unity エディターインストーラーには 含まれていません

次の GitHub のページを Git Clone して、Assets 以下の NavMeshComponents フォルダをあなたの Unity プロジェクトの Assets 以下へ配置します。
github.com

NavMeshSurface コンポーネントが利用可能になるので、シーン内の空オブジェクトに次のように追加します。

f:id:simplestar_tech:20190105191205j:plain
NavMeshSurface コンポーネントを追加した GameObject のインスペクタ

もう一つ、ユーザースクリプトがゲームオブジェクトに割当たっていますが、こちらの実装は定期的に Bake ボタンを押すというものです。
以下の実装になっています。

NavMeshSurfaceBaker.cs

using System.Collections;
using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshSurface))]
public class NavMeshSurfaceBaker : MonoBehaviour
{
    void Start()
    {
        _surface = GetComponent<NavMeshSurface>();
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            _surface.BuildNavMesh();

            yield return new WaitForSeconds(5.0f);
        }
    }

    NavMeshSurface _surface;
}

実装の意味は、単に 5秒ごとに NavMesh を焼き直すというもの

計算に利用するCollier メッシュオブジェクトの集め方はいくつか用意されていて
All ですべてのオブジェクト、Volume で指定した範囲内のオブジェクトだけ、Child で Transform の子オブジェクトだけ、を使って焼き直しが走ります。
レイヤーマスクも指定できるので、なるべく簡素なメッシュを用意してそれに割り当てたレイヤーを利用すると 5秒に一回の計算量のスパイクを低くできます。

エージェントの操作

動的 NavMesh が作れても、経路計算の方法がわからなければ、宝の持ち腐れですね。
NavMeshAgent クラスを活用して経路計算を行ないますが、そちらの知識を確認したい場合はこちらの記事が分かりやすいのでどうぞ
nopitech.com

エージェントが宙に浮いちゃった!どうすれば良い?

f:id:simplestar_tech:20190105192908j:plain
NavMeshSurface を利用すると中空を歩いてしまう…

私の対処としては AgentBaseHeightCorrector.cs を次の実装で作り、キャラクターオブジェクトに適用しました。

using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class AgentBaseHeightCorrector : MonoBehaviour
{
    void Start ()
    {
        _nav = GetComponent<NavMeshAgent>();
    }

    void Update()
    {
        CorrectBaseHeight();
    }

    private void CorrectBaseHeight()
    {
        NavMeshHit navhit;
        if (NavMesh.SamplePosition(transform.position, out navhit, 10f, NavMesh.AllAreas))
        {
            Ray r = new Ray(navhit.position, Vector3.down);
            RaycastHit hit;
            if (Physics.Raycast(r, out hit, 10f, LayerMask.GetMask("Level")))
            {
                _nav.baseOffset = -hit.distance;
            }
        }
    }

    NavMeshAgent _nav;
}

※実装ヒントは次の討論からもらいました。
https://forum.unity.com/threads/navmesh-surface-bakes-slightly-above-the-surface.508532/


毎フレーム処理が CPU リソース的に勿体ないなら、上の TimeUpdate の妙技を使ってみるのもありだと思います。

おわりに

うわぁ、ここまで書いてから類似するテラシュールブログさんの記事の存在に気付く…
ていうか自分スター押してる…忘れていたが正解でした!

tsubakit1.hateblo.jp

ただ、読み直した感じ、いくつか現在の実装とは異なる部分が見られたので
本記事は新しい NavMeshComponents の動作確認を行ったという意味があったかも

Unity:改造方法の確認

前書き

ゲーム開発者会議などで、ゲームユーザーの100人に一人はゲームを改造して不正を行っていることが明らかになっています。
Unityを利用して企業や個人でゲームをリリースしていますが、それらのゲームが容易に改造されなように対策されていることを確認することは
その後の楽しいゲーム体験が損なわれないことを保証するうえで重要なことです。

例えば、不正に報酬を得るプレイヤーにランキング上位を独占されたり、対戦時に不当な負け方を強いられたりしたら、そのゲームを遊びたくなくなります。
そんなわけで、今回は Unity 製ゲームの改造方法の一つを学び、自分のゲームが対策されていることを確かめる手段を覚えてみます。

ゲームロジックの記述されているファイル

こちらの記事を参考にします。
baba-s.hatenablog.com

直近で作った VRM ランタイムロードの改造が可能か確認することを目的に具体的な作業を記録していきます。
Windows ユーザーに届けるように作ったパッケージは複数のファイルで構成されています。

f:id:simplestar_tech:20190105132428j:plain
Build指定先のフォルダの様子
ゲーム名_Data フォルダを開いてみましょう。
f:id:simplestar_tech:20190105132518j:plain
ゲーム名_Data フォルダの様子
ハイライトしたように Managed フォルダがあります。
この中の Assembly-CSharp.dll ファイルにゲームのロジックが記録されています。

.dll ファイルの中身の確認

ゲームロジックが記録されている Assembly-CSharp.dll の中身を確認するツールを手元に用意しましょう。
GitHub - 0xd4d/dnSpy: .NET debugger and assembly editor
こちらのリリースの最新版を取得して起動します。

File > Open メニューから Assembly-CSharp.dll を開いて、中身を見てみます。

f:id:simplestar_tech:20190105134957j:plain
GameManager クラスの実装を表示している様子

すべてのゲームロジックがユーザーに公開されてしまっていることがこの確認操作でわかりました。

改造してみる

右クリックメニューから Edit Method が選べたので、こちらでLoadするファイル名を Vita から sendagaya_xiv に変更しました。
右下の Compile ボタンを押して成功したので、File > Save All で保存確認ダイアログが出るので OK を押して上書きします。

以上…

ゲームを起動すると、全く違うキャラクターをロードしてゲームが始まってしまいました。

f:id:simplestar_tech:20190105143716j:plain
改造後にゲームを起動すると Vita ではなく違うキャラがゲーム内に登場した

おわりに

PlayerSettings でMono から IL2CPP に切り替えると、ビルド時間は伸びますが難読化にはなる様子
ですが、オンラインゲームのような不正を行うと他のプレイヤーに害が及ぶようなケースでは何らかの対策を入れて、可能な限り不正の芽は摘んでおきたいところですね。