アマゾンバナーリンク

ディスプレイ広告

スポンサーリンク

TPSオンラインゲーム開発の教科書1

2021年5月7日

こんにちは!ジェイです。本日からさっそくTPSオンラインゲームの開発を開始します。それと同時に作り方の説明をなるべくわかりやすく解説していきます。

対象者は

  • UnityとC#は理解できている
  • PhotonCloudPUN2の基礎的な知識がある
  • 本格的なマルチプレイTPSゲームを作ってみたい人

ある程度の知識があることが前提になるので、まだ見てないのであれば以下の記事を一目見るとよいです。

PhotonPUN2の全体的な知識を知りたい方向け

PhotonCloudPUN2で3DActionGame開発したい方向け

記事内広告

開発するTPSオンラインゲームの仕様

  • マルチプレイで周りから来る敵を倒すステージごとにだんだん敵が強くなる
  • お金をためてアイテムや武器をパワーアップさせられる。
  • Waveごとに敵の数が決まっていて3DマップでTPS視点
  • 8人まで動かせるようにする
  • 協力プレイで全滅しないでWaveをクリアする
  • 1人でも残っていてWaveをクリアすると次のWaveに行ける
  • 全滅するとゲームオーバー
  • マッチングなしのいつでも入手可能

後にVRMやチームプレイなどの機能も実装しますが、主に解説するのは上記の項目です。ゲームを作ろうと思った経緯を知りたい方は以下を参照。

準備

PhotonCloudPUN2

一番最初に以下を参考にPhotonCloudPUN2を使う準備をします。

モデルとアニメーション

次に使うモデルを準備します。これはお好みのモデルで大丈夫ですが、UnityChanなどヒューマノイド対応のモデルにしましょう。インポートするデータにはLatifaさんを使わせていただきます。

こちらからアニメーションをインポートして準備完了です。

まずはオフラインで動かしてみる

さっそくですが、オフラインでキャラクターを動かします。まずは使用するモデル(Latifa V2 Prefab)にCharacter Controllerコンポーネントをアタッチして、以下のC#スクリプトをアタッチして、インスペクターで値を調整します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MultiplayWaveShooter;
using Photon.Pun;
using Photon.Realtime;

namespace MultiplayWaveShooter
{
    public class CPlayerController : MonoBehaviourPun
    {
        public enum EPlayerState
        {
            Idle, // アイドル状態
            Walk, // 歩き
            Jump, // ジャンプ
            Cover, // 壁の裏に隠れるカバーアクション
        }
        public EPlayerState PlayerState = EPlayerState.Idle;
        //移動速度等のパラメータ用変数(inspectorビューで設定)
        public float WalkSpeed;
        public float RunSpeed;
        private float NowSpeed;         //キャラクターの移動速度
        public float JumpSpeed;     //キャラクターのジャンプ力
        public float RotateSpeed;   //キャラクターの方向転換速度
        public float Gravity = 20.0f;      //キャラにかかる重力の大きさ
        public float SpeedSmoothing = 10.0f;
        public float AirControlAcceleration = 3.0f;

        //移動処理に必要なコンポーネントを設定
        private Animator _Animator;                 //モーションをコントロールするためAnimatorを取得
        private CharacterController _CharacterController;    //キャラクター移動を管理するためCharacterControllerを取得
        [HideInInspector]
        public float Horizontal = 0.0f;
        [HideInInspector]
        public float Vertical = 0.0f;
        public Vector3 TargetDirection;        //移動する方向のベクトル
        public Vector3 TargetRotateDirection;
        public Vector3 MoveDirection = Vector3.zero;
        public bool IsGrounded;
        private Vector3 AirVelocity = Vector3.zero;
        private float MoveSpeed = 0.0f;
        private float VerticalSpeed = 0.0f;
        private bool IsAiming = false;                                        // Boolean to get whether or not the player is aiming.
        private bool IsAimBlocked;
        private int hFloat;
        private int vFloat;
        void Start()
        {
            NowSpeed = WalkSpeed;
            hFloat = Animator.StringToHash("H");
            vFloat = Animator.StringToHash("V");
            _Animator = GetComponent<Animator>();
            _CharacterController = GetComponent<CharacterController>();
        }
        bool MoveFlag = false;
        float JumpTime = 0.0f;
        float JumpInterval = 3.0f;
        float TotalTime = 0.0f;
        float Sqrt2 = 1 / Mathf.Sqrt(2);
        void Update()
        {
            //★進行方向計算
            //キーボード入力を取得
            Vertical = Input.GetAxis("Vertical");         //InputManagerの↑↓の入力       
            Horizontal = Input.GetAxis("Horizontal");       //InputManagerの←→の入力 
            Vector3 forward = Vector3.zero;
            Vector3 right = Vector3.zero;
            Camera main_camera = Camera.main;
            if (main_camera)
            {
                //カメラの正面方向ベクトルからY成分を除き、正規化してキャラが走る方向を取得
                forward = Vector3.Scale(main_camera.transform.forward, new Vector3(1, 0, 1)).normalized;
                right = main_camera.transform.right; //カメラの右方向を取得
            }
            float vx, vy;
            MoveFlag = (Vertical == 0 && Horizontal == 0) ? false : true;
            switch (PlayerState)
            {
                case EPlayerState.Idle:
                    if (TargetDirection != Vector3.zero)
                    {
                        PlayerState = EPlayerState.Walk;
                    }

                    //Jumpボタンでジャンプ処理
                    if (Time.time > JumpTime + JumpInterval && !IsAiming && _CharacterController.isGrounded && Input.GetButton("Jump"))
                    {
                        JumpTime = Time.time;
                        PlayerState = EPlayerState.Jump;
                        VerticalSpeed = Mathf.Sqrt(2.0f * Gravity * JumpSpeed);
                        _Animator.SetBool("jump", true);
                        //_AudioSource.PlayOneShot(_AudioClip[3]);
                    }

                    if (Horizontal != 0 && Vertical != 0)
                    {
                        vx = Mathf.Abs(Horizontal) * Sqrt2;
                        vy = Mathf.Abs(Vertical) * Sqrt2;
                    }
                    else
                    {
                        vx = Mathf.Abs(Horizontal);
                        vy = Mathf.Abs(Vertical);
                    }
                    _Animator.SetFloat("Speed", Mathf.Min(1.0f, (vx + vy)), 0.1f, Time.deltaTime);
                    break;
                case EPlayerState.Walk:
                    if (!MoveFlag)
                    {
                        PlayerState = EPlayerState.Idle;
                    }

                    NowSpeed = (Input.GetKey(KeyCode.LeftShift) && !IsAiming) ? RunSpeed : WalkSpeed;

                    //Jumpボタンでジャンプ処理
                    if (Time.time > JumpTime + JumpInterval && !IsAiming && _CharacterController.isGrounded && Input.GetButton("Jump"))
                    {
                        JumpTime = Time.time;
                        PlayerState = EPlayerState.Jump;
                        VerticalSpeed = Mathf.Sqrt(2.0f * Gravity * JumpSpeed);
                        _Animator.SetBool("jump", true);
                        //_AudioSource.PlayOneShot(_AudioClip[3]);
                    }

                    if (Horizontal != 0 && Vertical != 0)
                    {
                        vx = Mathf.Abs(Horizontal) * Sqrt2;
                        vy = Mathf.Abs(Vertical) * Sqrt2;
                    }
                    else
                    {
                        vx = Mathf.Abs(Horizontal);
                        vy = Mathf.Abs(Vertical);
                    }
                    _Animator.SetFloat("Speed", Mathf.Min(1.0f, (vx + vy)), 0.1f, Time.deltaTime);
                    break;
                case EPlayerState.Jump:
                    if (_CharacterController.isGrounded)
                    {
                        if (_Animator != null)
                            _Animator.SetBool("jump", false);
                        if (!MoveFlag)
                        {
                            PlayerState = EPlayerState.Idle;
                        }
                        else
                        {
                            PlayerState = EPlayerState.Walk;
                        }
                    }
                    break;
            }

            //カメラの方向を考慮したキャラの進行方向を計算
            TargetDirection = Horizontal * right + Vertical * forward;
            TargetRotateDirection = TargetDirection.normalized;
            RotationControl(TargetRotateDirection); //旋回用関数 

            float cur_smooth = SpeedSmoothing * Time.deltaTime;
            float target_speed = Mathf.Min(TargetDirection.magnitude, 1.0f);
            MoveSpeed = Mathf.Lerp(MoveSpeed, target_speed * NowSpeed, cur_smooth);

            //★地上にいる場合の処理
            // ここが地面にずっといてもTrueになったりFlaseになったりするのを確認2018/5/23
            // 原因はMoveDirectionのy軸も一緒に計算して_CharacterController.Moveに渡してたことみたい
            // xz軸の移動のみMoveDirectionに入れy軸は別に計算する仕様にした
            IsGrounded = _CharacterController.isGrounded;

            if (_CharacterController.isGrounded)
            {
                //移動のベクトルを計算
                MoveDirection = TargetDirection.normalized;
                AirVelocity = Vector3.zero;
            }
            else//空中操作の処理(重力加速度等)
            {
                // 空中での動き
                if (TargetDirection != Vector3.zero)
                {
                    AirVelocity = TargetDirection.normalized * Time.deltaTime * AirControlAcceleration;
                }
                VerticalSpeed -= Gravity * Time.deltaTime;
            }
            //最終的な移動処理
            Vector3 movement = MoveDirection * MoveSpeed + new Vector3(0, VerticalSpeed, 0) + AirVelocity;
            movement *= Time.deltaTime;
            _CharacterController.Move(movement);
        }
        Vector3 NewDir = Vector3.zero;
        //キャラクターが移動方向を変えるときの処理
        void RotationControl(Vector3 rotate_direction)
        {
            rotate_direction.y = 0;

            //それなりに移動方向が変化する場合のみ移動方向を変える
            if (rotate_direction.sqrMagnitude > 0.01)
            {
                //緩やかに移動方向を変える
                float step = RotateSpeed * Time.deltaTime;
                NewDir = Vector3.Slerp(transform.forward, rotate_direction, step);
                transform.rotation = Quaternion.LookRotation(NewDir);
            }
        }
    }
}

上記のコードでキャラクターをawsdで動かして、スペースキーでジャンプできるようになります。

インスペクターの設定

実行結果

#Unity
TPSオンラインゲーム開発1日目
・歩いてジャンプの実装

https://gyazo.com/c23bf46f5b4949030f0b4d3f28fdcc59

アイコンは鬼塚@取締役さんからお借りしました。

Originally tweeted by ジェイ@Unityオンラインゲームスペシャリスト (@JY20160816) on 2021年5月4日.

+1