見出し画像

【備忘録】unity1week「かえす」に参加!

unityroomで不定期に開催されているunity1week(Unity1週間ゲームジャム)に参加しました。今回で9回連続9回目になります。今回の期間は2024年8月12日から8月18日までで、テーマは「かえす」でした。

今回も気づいたことや学んだことを備忘録にしていきます。


1.タイルマップ素材の設定と配置について

まず今回は以下の素材を購入し、タイルマップとして使用しました。まあまあのお値段はしますが、入っている素材の数を考えると安すぎます。買いです。DLSiteのセールかクーポン配付時が狙い時。

今回は使ってませんが、こちらのサイトもおすすめです(ドット絵世界はトップページのリンクは切れているようです)。

また、東方Projectのドット絵素材は以下のサイトからお借りしました。

ツクールストアも使えそうです(ただし、ツクール以外は使用不可の素材も多いので注意)。

今回の作品ではGridの子オブジェクトを4つ作りました。
それぞれの設定を簡単に説明します。
なお今回の主人公の魔理沙のSprite Rendererのレイヤーの順序は3ですので、これ未満だと魔理沙より下、これより上だと魔理沙の上に被って描画されます。

魔理沙様がレイヤー番号3番だ

(1)Floor

一番下に描画されます。レイヤーの順序は0。ステージ全体に敷き詰めます。

(2)Object_row

障害物としてコライダーは持ちますが、魔理沙より下に描画されます。レイヤーの順序は2。本棚の一番下や壁の一番下を作るときは、このタイルマップにタイルを配置します。
またTileMap Collider 2DとComposite Collider 2Dをアタッチします。TileMapColider2Dの「複合で使用」にチェックを入れます。TileMap Collider 2Dだけでも作動はしますが、Composite Collider 2Dをアタッチして、「複合で使用」にチェックを入れないと、1グリッドで一つのコライダーがついている状態になり、キャラクターが引っ掛かることがあります。

これ以上、上には進めませんが、本棚より帽子が
上に描画されています。

(3)object

普通の障害物。object_rowと同じ設定で、レイヤーの順序のみ4に設定します。

(4)object_high

通り抜けることはできるが、プレイヤーより前に描画されます。今回は本棚の一番上の列に採用しました。
コライダーはアタッチせず、レイヤーの順序を20に設定しました。

隠れる魔理沙

2.Fang Auto Tileを使ってウディタのタイルマップ素材を配置

「WOLF RPGエディター」、通称ウディタは無償で使用できるゲームエンジンで、日本ではかなり普及しており、フリーゲームでは使用率が高いです。
ウディタ用のタイルマップをUnityで使用することは、その素材の規約上問題なければ可能ですが、以下のような形式で配布されている場合があります。

これは壁用の素材なのですが、32×32ピクセルでスライスすると、5つのタイルしかできません。
配置するためには、「Fang Auto Tile」という無料アセットを使用します。使用方法は以下の記事を参照してください。

注意点は、壁のようにコライダーをアタッチして使用する場合は、コライダータイプを設定する必要があることです。
グリッド全体にコライダーをつける場合はグリッド、透過部分には付けない場合はスプライトを選択します。

そして、Generateで出来たTextureのほうではなく、もともとあったFang Auto Tileをタイルパレットに追加してください。
以下の画像のように1マスだけのタイルになります。

Wall Fang Auto Tileをタイルパレットにドラッグアンドドロップ

これをタイルマップで描くと、自動的に内外や上下を判断して、タイルマップが配置されます。

ウディタ恐るべし

3.A* Pathfinding Project Proで追ってくる敵を作る

障害物があっても追ってくる敵を作れますが、かなり難しかったです。

(1)障害物用のタイルマップを作る

真っ白の正方形のタイルマップを用意し、障害物の上から塗りたくります。塗り終わったらTileMapRendererを非表示にすると、見えなくなります。オブジェクトごとSetActive(false)にすると、経路探索ができなくなります。
レイヤーをDefault以外の適当なレイヤーにしてください。ここではLayer1にします。このオブジェクトにTilemap Collider2Dをアタッチします。

(2)AstarPathを配置する

空のオブジェクトにAstarPathをアタッチして、以下のように設定し、Scanをクリックします。
WidthとDepthで敵が移動できる範囲を設定します。
Node sizeはマス目の細かさです。これが低いほど細かい動きが出来ますが、ギリギリを攻めて、引っ掛かりやすくなるので、調整が必要です。
Obstacle Layer Maskは通行ができなくなるレイヤーです。
これを先ほど設定したLayer1にして、Scanをクリックすると…。

以下のように、水色の敵が通れる場所、赤の敵が通れない場所と区分けをしてくれます。

(3)敵にAIPathとSeekerをアタッチする

以下のように設定します。
AIPathのOrientationはYAisForward(for 2Dgames)にしないと2Dゲームでは、まったく動かないので注意しましょう。

なお、敵味方ともにコライダーはCircle Collider 2Dを推奨します。角に引っ掛かることが少なくなります。
またコライダーのマテリアルに摩擦係数を低くした、物理マテリアルをアタッチすることも、ある程度効果はあると思います。

Friction(摩擦係数)は1が最大で滑らない

(4)スクリプト(一部)

using UnityEngine;
using Animancer;
using Pathfinding; // A* Pathfinding Project を使用

public class EnemyController : MonoBehaviour
{
    private Rigidbody2D rb;
    private AnimancerComponent animancer;
    private AIPath aiPath; // A* Pathfinding Project の AIPath コンポーネント

    [Header("Movement Settings")] 
    [SerializeField] private float moveSpeed = 2f;  // パトロール時の移動速度
    [SerializeField] private float chaseSpeed = 3f;  // プレイヤー追跡時の移動速度
    [SerializeField] private float returnSpeed = 2f;  // 元の位置に戻る際の移動速度

    [Header("View Settings")] 
    [SerializeField] private GameObject upView;  // 上方向の視界オブジェクト
    [SerializeField] private GameObject downView;  // 下方向の視界オブジェクト
    [SerializeField] private GameObject leftView;  // 左方向の視界オブジェクト
    [SerializeField] private GameObject rightView;  // 右方向の視界オブジェクト

    [Header("Chase Settings")] 
    [SerializeField] private float chaseDurationAfterLost = 3f;  // プレイヤーを見失った後に追跡を続ける時間
    private float chaseTimer;  // 追跡の残り時間を管理するタイマー

    [Header("Idle Rotation Settings")] 
    [SerializeField] private 巡回モード patrolMode = 巡回モード.巡回する;  // 巡回モード(巡回するかしないか)
    [SerializeField] private 回転方向 rotationDirection = 回転方向.右回り;  // 回転方向(右回りか左回りか)
    [SerializeField] private float idleRotationInterval = 2f;  // 停止中に方向を変える間隔
    private float idleRotationCounter;  // 次に回転するまでの時間を管理するカウンター

    private Transform target;  // プレイヤーのTransformへの参照
    private bool isChasing;  // プレイヤーを追跡中かどうかのフラグ
    private bool isReturning;  // 元の位置に戻る途中かどうかのフラグ

    /////////////////////////中略///////////////////////

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
        animancer = GetComponent<AnimancerComponent>();
        aiPath = GetComponent<AIPath>(); // AIPath コンポーネントを取得

        aiPath.canMove = true; // 初期状態でCan Moveを有効に設定
        Debug.Log("Can Move is set to: " + aiPath.canMove); // デバッグログ

        initialPosition = transform.position;  // 初期位置を保存

        target = GameObject.FindGameObjectWithTag("Player").transform;  // プレイヤーをターゲットとして取得

        if (patrolMode == 巡回モード.巡回しない)
        {
            idleRotationCounter = idleRotationInterval;  // 巡回しない場合は回転カウンターを初期化
        }

        currentIdleClip = idleDown;  // 初期状態で下向きの待機アニメーションを設定
        animancer.Play(currentIdleClip);  // 初期の待機アニメーションを再生
        UpdateViewDirection();  // 視界方向を更新
    }

    void Update()
    {
        if (isChasing)
        {
            aiPath.maxSpeed = chaseSpeed;  // 追跡時の速度を設定
            ChasePlayer();  // プレイヤーを追跡
        }
        else if (isReturning)
        {
            aiPath.maxSpeed = returnSpeed;  // 帰還時の速度を設定
            ReturnToPosition();  // 初期位置に戻る
        }
        else
        {
            if (patrolMode == 巡回モード.巡回する)
            {
                Patrol();  // 巡回動作を行う
            }
            else
            {
                IdleAndRotate();  // 停止しつつ回転を行う
            }
        }
    }

    private void Patrol()
    {
        if (moveDir == Vector2.zero)
        {
            IdleAndRotate();  // 移動方向が設定されていない場合は回転動作を行う
            return;
        }

        moveDir = (target.position - transform.position).normalized;  // プレイヤー方向に向かう
        aiPath.destination = target.position; // 目的地をプレイヤーの位置に設定
        aiPath.maxSpeed = moveSpeed;  // 巡回時の速度を設定

        UpdateCurrentDirection();  // 現在の方向を更新
        PlayMoveAnimation();  // 移動アニメーションを再生
    }

  /////////////////////////中略///////////////////////

    private void ChasePlayer()
    {
        aiPath.maxSpeed = chaseSpeed; // 追跡時の速度を設定
        moveDir = (target.position - transform.position).normalized;  // プレイヤー方向に向かう
        aiPath.destination = target.position;  // 目的地をプレイヤーの位置に設定

        UpdateCurrentDirection();  // 現在の方向を更新
        PlayMoveAnimation();  // 移動アニメーションを再生

        // プレイヤーと一定距離が離れた場合、追跡を終了し元の位置に戻る
        if (Vector3.Distance(transform.position, target.position) > 1f)
        {
            chaseTimer -= Time.deltaTime;
            if (chaseTimer <= 0)
            {
                isChasing = false;
                isReturning = true;
            }
        }
    }

    private void ReturnToPosition()
    {
        aiPath.destination = initialPosition; // 目的地を初期位置に設定
        aiPath.maxSpeed = returnSpeed;  // 帰還時の速度を設定

        UpdateCurrentDirection();  // 現在の方向を更新
        PlayMoveAnimation();  // 移動アニメーションを再生

        // 初期位置に到達したら停止し、待機アニメーションを再生
        if (Vector3.Distance(transform.position, initialPosition) < 0.2f)
        {
            isReturning = false;
            rb.velocity = Vector2.zero;  // 移動を停止

            currentDirection = Vector2.down;  // 初期位置での方向を下に設定
            PlayIdleAnimation();  // 待機アニメーションを再生
            UpdateViewDirection();  // 視界方向を更新
        }
    }

    private void UpdateCurrentDirection()
    {
        // AIPath の目標地点に基づいて現在の移動方向を決定
        Vector3 direction = aiPath.steeringTarget - transform.position;

        if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
        {
            currentDirection = (direction.x > 0) ? Vector2.right : Vector2.left;
        }
        else
        {
            currentDirection = (direction.y > 0) ? Vector2.up : Vector2.down;
        }

        UpdateViewDirection();  // 視界方向を更新
    }

////////////////////////////////中略/////////////////////////

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            isChasing = true;
            aiPath.canMove = true; // プレイヤーを追いかけ始める
            chaseTimer = chaseDurationAfterLost;
        }
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            isChasing = true;
            aiPath.canMove = true; // プレイヤーを追いかけ続ける
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.CompareTag("Player"))
        {
            chaseTimer = chaseDurationAfterLost; // プレイヤーが視界から出た後の追跡時間を設定
        }
    }
}

結果、以下のような敵が作れます。

絶対魔理沙を許さない小悪魔

4.Layer Manager Yaeを使って、レイヤーを可視化する

積みアセットになっていた「Layer Manager Yae」ですが、今回導入したところ、結構便利だと思いました。

レイヤーの順序を以下のように可視化してくれます。
このソーティングレイヤーウィンドウ上で、レイヤーの順序を入れ替えることも可能です。

5.AnimancerProを利用して、キャラクターを動かしてみる

以前、AnimancerProについての記事を書きましたが、今まではボタンのアニメーションにしか使っていなかったので、今回初めてキャラクターアニメーションに使ってみました。

何回かエラーは出ましたが、やはりスクリプト上だけで完結するのが非常に良いです。有名アセットなので、ChatGPTも「Animancer Proを使ってキャラクターを上下左右に動かしたい」等の指示をすると対応してくれます。

またSolo Animationコンポーネントがとてもよいです。
一つのアニメーションを繰り返しさせたいオブジェクトにアタッチするとすぐ実装できます。

PLYAER、ENEMYのマーカー、チェックポイントの光をSolo Animationで実装。

速度も変更できます。楽すぎィ!

ENEMY、PLAYERマーカーはぴぽや倉庫でおなじみのぴぽさんのニコニ・コモンズ素材を使いました。非常に使い勝手がいいです。

6.UIを少しこだわってみた

なんかいつもUIがイマイチだったので、おしゃれなUIでおなじみの空想曲線さんのUIを購入しました。

いつもよりは良くなった気がしますが、活かしきれてないような気がします。

タイトル画面は結構うまく作れた

UIの拡大伸縮については以下の記事が参考になります。
ウィンドウは、せっかくいい素材を手に入れても、枠が細くなったりして台無しになることがありますからね。

7.VRM Posing DeskTopを使ってVRoidを撮影する

前回のunity1weekに投稿した作品もVRoidを撮影して素材として使っていたのですが、VRoidStudio上で撮影していたので非常に苦労しました。

今回は「VRM Posing Desktop」を使用したので、非常に楽でした。

ソフトの使い方は直感的なので、あまり迷いませんが、以下のサイトが一番詳しいかなと思います。

以下がソフトの画面です。

パチュリー外に出る

このソフトの主な利点として感じたのは、

  1. 表情が保存できる

  2. 何人ものVRoidを一緒に撮影できる

  3. 画像だけでなく、3Dや動画(fbx、mp4など)も取り込める

  4. ポーズが多い。ワークショップから無料でポーズがダウンロードできる。

  5. ポーズの一から作ることもできるし、微調整もできる。

  6. 簡単にポストエフェクトをかけられる。

  7. 現在の環境を保存できる。

  8. 動作が軽い。

  9. 比較的価格が安い(通常時1400円、セール時に800円だった)。

デフォルトで剣や銃の3Dモデルが入ってます

表情が保存できるのと、動作が軽いのが大きすぎます。
VRoidStudioは置いといて、UnityやCLIP STUDIO PAINTでもある程度のことはできますが、VRoidの撮影に特化しているわけではないので、物足りない部分があります。

ちょっと惜しい点はポーズのアイコンが分かりにくい点でしょうか。赤いデッサン人形の全体画像なんですが、小さすぎてどんなポーズかイマイチわかりにくいんですよね。

現☆行☆犯

あと「このキャラクターを一時的に非表示にしたい」というときにチェックボックスなどがなくて、VRoidを選択→「メッシュ」→「全隠し」としなといけないので迷いますね。

魔理沙を消す方法

図書館の3Dモデルはプリメロ工房さんのモデルを使用しました。高品質の3Dモデルが無料で使えるのでおススメです!

VRoidはOSONOさんの商品を購入しました。
東方キャラが結構な数揃っています。

8.TimeLineで音が鳴らなかったので、応急処置をした

WebGLでビルドしたらTimeLine上で音が鳴らない現象が発生しました。同じような現象をポストされている方がいらっしゃったので、Unityのバージョン(今回使用したのは2022.3.42f)に起因すると思われます。

他のバージョンもいくつか試しましたが、解決しなかったため、TimeLine上で音を鳴らすのではなく、TimeLineにシグナルエミッターを設置し、オブジェクトのスクリプトを実行して、サウンドを鳴らすことにすると、WebGLでも音が再生されました。

TimeLine上のオーディオソースがWebGL環境では再生されなかった
シグナルエミッターを0.50付近に設置し、無理やり再生した
シグナルレシーバーはこんな感じ

9.上手くいかなかったこと

ゆーりんちさんのエフェクトセット買っていたので、今回使おうと思ってましたが、マテリアルの設定の仕方がすぐにはわからなかったので、ゆっくり勉強して別の機会に使うことにしました。

この間、宴を会話シーンとして使う実験をして、成功したのですが、今回はまた使えなくなってしまいました。会話シーンが短いので、脱出ゲーム作成時に自作した会話システムを流用しました。

宴は本当に便利なので、もっと楽に組み込めるようになりたいです。

とりあえず、積みアセットからはA* Pathfinding Project ProとLayer Manager Yaeを使用し、AnimancerProも本格的に使ったので、良かったです。

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