株式会社グローバルゲート公式ブログ

Unityで3Dゲームに挑戦~ユニティちゃんにアキバを走らせる~

こんにちは、株式会社グローバルゲートのモーリーです。 
 
楽しみにしていたお盆休みも気づけばあっという間に過ぎ去り、ほんの少し…本当に少しだけですが秋の気配を感じるときもあるような、ないような。
とはいえ、日中はまだ厳しい暑さが続いていますので、皆さまも熱中症など体調管理にはくれぐれもご注意ください。 

さて、今回はUnityを使って3Dゲームの制作に挑戦してみます。 
前回の記事では2Dシューティングゲームに挑戦しましたが、Unityは3Dゲームの制作にも対応しています。今回は無償/有償で手に入るアセットを活用しつつ、実際に3Dのアクションゲームを作ってみたいと思います。 

Unityで3Dアクションゲームを作る

今回はユニティちゃんが秋葉原の街を探索できるようにするところまでを目指してやってみたいと思います。 

ちなみに:ユニティちゃんとは

Unity開発元の日本法人であるユニティ・テクノロジーズ・ジャパンによって作られたオリジナルキャラクターです。 
本名は大鳥こはく、カレーコロッケが好き。 
 
ちなみに2013年に誕生したかなり古参のキャラクターです。

公式サイト

プロジェクトの作成

※ Unity Hubのインストールとプロジェクトの作成は前回の記事もあわせてご参照ください。 

Unity Hubを起動し、New Projectをクリックして新しいプロジェクトを作成します。
今回は3DゲームをつくりたいのでテンプレートでUniversal 3Dを選択します。 

ちなみにHigh Definition 3Dはよりハイクオリティでリアルな3Dゲームを作りたいときのテンプレートです。 

プロジェクトの作成が完了するとエディタが起動します。 
2Dのときと異なり、3D画面が広がっています。

アセットのダウンロードとインポート

アセットストアからのダウンロードとインポート

まずはアセットストアから街並みのアセットをダウンロードし、配置してみましょう。 
 
今回はゼンリンが提供している秋葉原の街を使ってみたいと思います。 
Unity Asset StoreJapanese Otaku Cityと検索してください。 

詳細ページを開き、マイアセットに追加するをクリックします。

Unityエディタに戻り、上部メニューよりMy Assetsを選択します。 

Package Managerが開きますので、先程のJapanese Otaku Cityを選択し、DownloadImportの順にクリックします。 

Import Unity Packageのウインドウが開きますので、Importをクリックします。 

Importが完了すると、Projectタブの中にAssets配下にZRNAssetsというフォルダが作成されています。
この中に街並みのオブジェクトやテクスチャ、サンプルファイルなどが入っています。

実際にアキバの街並みのファイルを開いてみます。

この中の005339_.....ScenesSample_00...というファイルをダブルクリックして開きます。 
(フォルダ名はアセットのバージョンによって異なる可能性があります)

開き…ましたがピンク色のシルエットが表示されるだけで、これで正しいの?と思う画面になりました。 

このピンクはUnityのバージョン差異によってテクスチャーが読み込めない状態を意味します。 
ゼンリンのアセットは2018年リリースで今となっては古いものですので、仕方がありません。 
これを修正して最新のUnityで使えるようにします。

最新のUnityで使えるように修正する 

※この工程は最新のUnityに対応したアセットでは必要ありません。

プロジェクトタブでMaterialを開きます。 
本来ならテクスチャーのプレビューが表示されますが、すべてピンクの丸になっています。 

このファイルを選択した状態でEditRenderingMaterialsConvert Selected Built-in Materials to URPを選択します。 
(URPはUniversal Render Pipelineの略で、最新のUnityで使用されている新しいレンダリング方式です) 

これである程度はテクスチャのプレビューが表示されました。 
まだいくつかピンクの丸状態のマテリアルが残っているので、これを手作業で修正します。 

先程のConvert...で修正できなかったマテリアルは1つずつ修正する必要があります。
マテリアルの1つを選択し、Inspectorを確認します。

Main Mapsに選択されている画像をダブルクリックするとProjectタブ上でハイライトされますので、このファイルが何かを覚えておきます。

次にShaderの選択をUniversal Render PipelineLit に変更します。

Base Mapの◉をクリックし、Main Mapsで選択されていたファイルを選択します。 
同時に、Surface OptionAlpha Clippingのチェックをオンにします。 

この作業をピンクの丸になっているマテリアル分繰り返して…

ようやく街が表示されました!
最新版のUnityに対応しているアセットなら読み込むだけですぐ使えると思います…

現状はアセット内のサンプル用シーンファイル上で作業を行っていましたので、メインのシーンファイルに街並みのみ移しておきます。 
  
Hierarchyの中のPQ_Rewmake_AKIHABARATexturePlusをコピーし、AssetsScene内のSampleSceneを開き、Hierarchy内でペーストします。これで街並みのみが使えるようになりました。 

外部サイトからのアセットのダウンロードとインポート

続いて、ユニティちゃんアセットをインポートします。 
ユニティちゃんはUnity Asset Store上のアセットではバージョンが古いため、公式サイトからダウンロードします。 

公式サイトのDATA DOWNLOADをクリックし、ライセンスや規約を確認して同意の上データをダウンロードするをクリックし、ユニティちゃん 3Dモデルデータ のDOWNLOADをクリックします。

 UnityChan_v1.4.0.unitypackageがユニティちゃんアセットです。

ダウンロードしたアセットをインポートします。
 
メニューのAssets Import PackageCustom Package...をクリックし、先程のファイルを選択します。

ゼンリンのアセットと同様にImport Unity Packageのウインドウが表示されますので、Importをクリックします。

ユニティちゃんもゼンリンのアセットと同様にピンクになっていますが、ユニティちゃんの場合はUnity Toon Shaderという拡張機能を入れることで解決します。

Package MangerのInstall package from git URLを選択し、com.unity.toonshaderと入力し、Installをクリックします。

ユニティちゃんがきちんと表示されました!

ステージにユニティちゃんを設置します。
プレハブ化されているオブジェクトがあるため、そちらを使いましょう。
 prefabs unitychan_dynamic.prefabを選択してHierarchyにドラッグします。

ちなみにunitychan.prefabというオブジェクトもありますが、こちらはアニメーションや動きが簡略された軽量バージョンとなります。

ユニティちゃんがアキバに降り立つ!…が、スケールがあっていませんので巨人になってしまいました。
街アセットを拡大し、サイズ感を合わせておきます。

HierarchyでPQ_Remake_AKIHABARAを選択し、InspectorのTransform Scaleで6倍にします。

これでちょうどいい感じになりました。

ユニティちゃんを縮小した場合、地面の判定などがおかしくなってしまったので街並みを拡大する方法を取りました。

ユニティちゃんを操作可能にする

いよいよユニティちゃんを操作できるようにしていきましょう。 
ユニティちゃんはキーボードの上キーで前に進み、下キーで後ろに振り向く、左右キーで向きを変える、スペースキーでジャンプ、Shiftを押しながらの矢印キーでダッシュという設定にしました。

Input Systemの追加

スペースキーの入力を受け取るようにInput Systemに追加します。
Assetsタブ内のInputSystem_Actionsをダブルクリックし、Input Systemウインドウを開きます。
 Actionsの+をクリックしてJumpという名称のアクションを追加し、BindingのPathでSpace(Keyboard)を選択します。

操作スクリプトの追加

次に操作用のスクリプトを記述します。
Assetsタブの中で右クリックし、Create Scripting Empty C# Scriptを選択します。
ファイル名はUnityChanKeyboardControllerとしておきます。

作成されたスクリプトファイルをダブルクリックしてエディタで開き、次のスクリプトを記述します。

// TPSPlayer_InputSystem.cs  (Unityちゃんに付ける) 
using UnityEngine; 
using UnityEngine.InputSystem; 
 
[RequireComponent(typeof(CharacterController))] 
public class TPSPlayer_InputSystem : MonoBehaviour 

    [Header("Move")] 
    public float walkSpeed = 2.8f; 
    public float runSpeed  = 5.2f; 
 
    [Header("Jump/Gravity")] 
    public float gravity    = -9.81f;   // 負の値 
    public float jumpHeight = 1.2f; 
 
    // --- 誤ジャンプ防止&安定化 --- 
    [Header("Jump Stability")] 
    [SerializeField] float coyoteTime   = 0.12f;  // コヨーテ 
    [SerializeField] float jumpBuffer   = 0.12f;  // バッファ 
    [SerializeField] float jumpCooldown = 0.12f;  // クールダウン 
 
    // --- 落下チューニング(追加)--- 
    [Header("Fall Tuning")] 
    [Tooltip("落下中(velocity.y<0)に重力へ掛ける倍率。2.0〜2.5くらいが目安")] 
    [SerializeField] float fallMultiplier = 2.0f; 
    [Tooltip("ジャンプ上昇中にジャンプ鍵を離したときの重力倍率(低めジャンプ)")] 
    [SerializeField] float lowJumpMultiplier = 2.0f; 
    [Tooltip("下向きの最大速度(m/s)。絶対値で制限)")] 
    [SerializeField] float maxFallSpeed = 40f; 
 
    [Header("Facing")] 
    [Tooltip("回転速度(度/秒)。小さいほどゆっくり向きが変わる")] 
    public float turnSpeed = 360f; 
    [Range(0f, 1f)] 
    [Tooltip("横入力に対してどれだけ向きを合わせるか。0=全く向けない(完全ストレイフ), 1=今まで通り")] 
    public float strafeFacingWeight = 0.2f;  // まずは 0.2 くらいがおすすめ 
 
 
    float coyoteTimer = 0f; 
    float bufferTimer = -999f; 
    float lastJumpAt  = -999f; 
 
    CharacterController controller; 
    Animator animator; 
 
    GameInput input;                     // 生成クラス 
    GameInput.PlayerActions player;      // ActionMap: Player 
 
    Vector3 velocity; 
 
    void Awake() 
    { 
        controller = GetComponent<CharacterController>(); 
        animator   = GetComponentInChildren<Animator>(); 
 
        input  = new GameInput(); 
        player = input.Player; 
    } 
 
    void OnEnable()  { input.Enable();  player.Enable(); } 
    void OnDisable() { player.Disable(); input.Disable(); } 
 
    void Update() 
    { 
        // --- 入力取得 --- 
        Vector2 mv      = player.Move.ReadValue<Vector2>();   // WASD / Lスティック 
        bool   sprint   = player.Sprint.IsPressed();           // Shift 
        bool   jumpDown = player.Jump.WasPressedThisFrame();   // 押した瞬間 
        bool   jumpHeld = player.Jump.IsPressed();             // 押しっぱ 
 
        // ジャンプ早押しバッファ 
        if (jumpDown) bufferTimer = jumpBuffer; 
 
        // --- カメラ基準の移動方向 --- 
        var cam = Camera.main; 
        Vector3 camF = Vector3.Scale(cam.transform.forward, new Vector3(1,0,1)).normalized; 
        Vector3 camR = cam.transform.right; 
        Vector3 moveDir = (camF * mv.y + camR * mv.x); 
        if (moveDir.sqrMagnitude > 1f) moveDir.Normalize(); 
 
        // --- 向き --- 
        if (moveDir.sqrMagnitude > 0.0001f) 
        { 
            // 横入力は向きにあまり影響させない(ストレイフ気味に) 
            Vector3 faceDir = (camF * mv.y) + (camR * (mv.x * strafeFacingWeight)); 
            if (faceDir.sqrMagnitude > 0.0001f) 
            { 
                var targetRot = Quaternion.LookRotation(faceDir.normalized, Vector3.up); 
                // 割合補間ではなく、1秒あたり turnSpeed 度までで回す(回し過ぎ防止) 
                transform.rotation = Quaternion.RotateTowards( 
                    transform.rotation, 
                    targetRot, 
                    turnSpeed * Time.deltaTime 
                ); 
            } 
        } 
 
 
        // --- 水平移動 --- 
        float speed = sprint ? runSpeed : walkSpeed; 
        controller.Move(moveDir * speed * Time.deltaTime); 
 
        // --- 接地(このフレーム固定) --- 
//        bool grounded = controller.isGrounded; 
        bool grounded = true; 
 
        // コヨーテ更新 
        if (grounded) coyoteTimer = coyoteTime; 
        else          coyoteTimer -= Time.deltaTime; 
 
        bool consideredGrounded = grounded || (coyoteTimer > 0f); 
 
        // 接地中の下向き速度を抑える 
        if (consideredGrounded && velocity.y < 0f) velocity.y = -2f; 
 
        // --- ジャンプ判定 --- 
        bool canJump = (jumpHeight > 0f) 
                    && (bufferTimer > 0f) 
                    && consideredGrounded 
                    && (Time.time - lastJumpAt >= jumpCooldown); 
 
        if (canJump) 
        { 
            lastJumpAt  = Time.time; 
            bufferTimer = -999f; 
            coyoteTimer = 0f; 
 
            // 物理で初速付与(見た目はアニメで) 
            velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity); 
 
            if (animator && HasParam("Jump")) animator.SetTrigger("Jump"); 
        } 
 
        // --- 重力(落下強化&低めジャンプ)※ここが追加の肝 --- 
        float g = gravity; 
        if (velocity.y < 0f)               g *= fallMultiplier;     // 落下中は強め 
        else if (!jumpHeld)                g *= lowJumpMultiplier;  // 上昇中にキー離したら強め 
 
        velocity.y += g * Time.deltaTime; 
 
        // 終端速度(最大落下速度)でクランプ 
        if (velocity.y < -maxFallSpeed) velocity.y = -maxFallSpeed; 
 
        controller.Move(velocity * Time.deltaTime); 
 
        // バッファ減衰 
        bufferTimer -= Time.deltaTime; 
 
        // --- Animator(任意)--- 
        if (animator) 
        { 
            float moveAmount = mv.magnitude; 
            float speed01    = Mathf.Clamp01(moveAmount) * (sprint ? 1f : 0.5f); 
            if (HasParam("Speed"))         animator.SetFloat("Speed", speed01, 0.1f, Time.deltaTime); 
            if (HasParam("Grounded"))      animator.SetBool("Grounded", consideredGrounded); 
            if (HasParam("VerticalSpeed")) animator.SetFloat("VerticalSpeed", velocity.y); 
        } 
    } 
 
    bool HasParam(string name) 
    { 
        if (!animator) return false; 
        foreach (var p in animator.parameters) if (p.name == name) return true; 
        return false; 
    } 

このスクリプトをInspectorタブにドラッグしてユニティちゃんにアタッチします。

カメラを追随させる

これでユニティちゃんを操作できるようになりましたが、このままではユニティちゃんが画面外に出てしまいどこにいるか分からなくなってしまいます。
そこでカメラにスクリプトをアタッチし、操作キャラを追いかけるようにします。

Hierarchyタブの中で右クリックし、Create Emptyを選択してGame Objectを作成します。
そしてそのGame ObjectをユニティちゃんPrefabの配下に置き、ユニティちゃんの頭あたりに来るように調整します。

次にAssetsの中で右クリックしてEmpty C# Scriptを選択し、空のスクリプトを作成します。名前はOrbitFollowCameraとします。
ダブルクリックでエディタを開き、次のスクリプトを入力します。

// FollowCameraNoMouse.cs  (Main Camera に付ける) 
using UnityEngine; 
 
public class FollowCameraNoMouse : MonoBehaviour 

    public Transform target;                         // Unityちゃん子の CameraTarget 
    public Vector3 offset = new Vector3(0, 1.4f, -4f); 
    [Range(-80, 80)] public float pitch = 15f;       // 仰角 
    public float yawOffset = 0f;                     // 右肩越し等にしたい時は±で調整 
    public float posSmooth = 10f;                    // 追従の滑らかさ 
    public float rotSmooth = 12f; 
    public float collisionRadius = 0.25f;            // 壁貫通防止 
    public LayerMask collisionMask = ~0; 
 
    void LateUpdate() 
    { 
        if (!target) return; 
 
        // プレイヤーの向き + オフセットでカメラの向きを決める(マウス入力なし) 
        float yaw = target.parent ? target.parent.eulerAngles.y + yawOffset 
                                  : target.eulerAngles.y + yawOffset; 
        Quaternion rot = Quaternion.Euler(pitch, yaw, 0f); 
 
        // 目標位置(ターゲットからオフセットぶん後方) 
        Vector3 desired = target.position + rot * offset; 
 
        // 壁貫通防止(簡易スフィアキャスト) 
        Vector3 from = target.position; 
        Vector3 dir  = desired - from; 
        float dist   = dir.magnitude; 
        if (Physics.SphereCast(from, collisionRadius, dir.normalized, out var hit, dist, collisionMask, QueryTriggerInteraction.Ignore)) 
            desired = hit.point - dir.normalized * (collisionRadius * 1.1f); 
 
        // スムーズ追従 
        float tPos = 1f - Mathf.Exp(-posSmooth * Time.deltaTime); 
        float tRot = 1f - Mathf.Exp(-rotSmooth * Time.deltaTime); 
        transform.position = Vector3.Lerp(transform.position, desired, tPos); 
        transform.rotation = Quaternion.Slerp(transform.rotation, rot, tRot); 
    } 

このスクリプトをMain Cameraにアタッチし、Targetでユニティちゃん配下に置いたGame Objectを選択します。

この時点で最低限の操作ができるようになりました!
ただ歩行モーションの設定をしていないため、地面を滑っているような動きになってしまいます。

ユニティちゃんアセットには歩行やジャンプのモーションがありますので、それを使えるようにします。

モーションの設定

ユニティちゃんを選択し、InspectorControllerUnityChanLocomotionsを選択します。

UnityChanLocomotionsの表記部分をダブルクリックし、Animatorタブを開きます。
このコントローラーは概ねこのままで動作するのですが、ジャンプの判定と静止状態からのジャンプがうまく動きませんでしたので、その点を修正します。

Parametersの設定

Parametersタブを開き、既存のJumpを削除し、+をクリックしてTriggerを選択、名称をJumpにします。

Transitionの追加

Idleを右クリックしてMake Transitionを選択し、矢印をJumpにつなげます。

この矢印をクリックし、ConditionsJumpを選択します。

これで一通り動くようになったのですが、現状は当たり判定を設定していないため、ユニティちゃんがビルを突き抜けてしまいます。
建物に当たり判定を設定し、この問題が起きないようにしましょう。

当たり判定を追加する

ユニティちゃんを選択し、Add ComponentからCharactor Contorollerを追加します。

次に建物オブジェクトをHierarchy上で選択し、Add ComponentからMesh Coliderを追加します。
(ゼンリンのアセットではBlock_◯というグループ配下が建物になっています)

これで壁を突き抜けることなく秋葉原をお散歩できます。

完成!

ということで、アキバの街を駆け抜けるユニティちゃんを完成させることができました。

実行ファイルはこちら(Windows版)
ZIPファイルをダウンロードし、3D.exeを実行すると遊べます。

ブラウザ用にビルドしてご紹介しようと思いましたがエラーが解決しませんでした…


なお、建物や高速道路などには当たり判定を追加しましたが、街の境界には何も設定していませんので市外に出ようとすると…

ユニティちゃぁぁぁん!

※ 操作不能になったらアプリを終了させてください。

まとめ

ということで、今回はアセットを活用しつつUnityで3Dアクションゲームの初歩の初歩を作ってみました。
これだけでもかなり苦労したので、ゲームとして完成させるにはどれほどの年月が必要になることやら…。

前回と今回の記事でゲーム開発の一端に触れることができましたが、その苦労をわずかでも垣間見たことで、どれほどひどいクオリティのゲームでも軽々しく笑うことができなくなりました。星をみるひとでもコンボイの謎でも、リリースまでには多くの苦労があったことでしょう。KOTYも見ていて心が痛む


当社ではUnityによるゲーム開発はお受けできませんが、ホームページへのCMS導入やリニューアルなどのご依頼については随時承っております。
お気軽にお問い合わせください。

【関連記事】

ご相談・お問い合わせ

当社サービスについてのお問い合わせは下記までご連絡下さい。

お電話でのお問い合わせ

06-6121-7581 / 03-6415-8161