今回は、前回使ったスクリプトの説明をします。
「とりあえず動けばいいや」というところが多いので、全体的なところはあまり参考にしない方が良いと思います。個々の技術については、ある程度調べたつもりですので、参考になれば幸いです。
ひととおり書いて見直しましたが、PlayerMove.cs 以外は、無駄な説明だったような気がします。
はじめに
私は最初に「ゲーム制作 オンライン3Dアクションゲームの作り方」という本でUnityの勉強をしました。この本で勉強した都合、ファイル名やメソッド名などが同じものになっています。似ていてもロジックはかなり違うので、同書籍で勉強した方はご注意ください。そのほかに、Udemy のコースや Youtubeに公開されいる動画、色々な方のブログなど、いろいろ混ざっています。
スクリプトの説明
ファイルについて
以下の4つのファイルから構成されています。
- PlayerAnimation.cs
- PlayerCtrl.cs
- PlayerMove.cs
- PlayerStatus.cs
では簡単な順に説明していきます。
namespace について
namespace を作って使うようにしています。これをやっておかないと、アセットストアからほかのアセットを入れた際に競合することがあります。
今回は Bonfire としました。
PlayerStatus.cs
プレイヤーの状態管理をするクラスです。
using UnityEngine;
using UnityEngine.UI;
namespace Bonfire
{
public class PlayerStatus : MonoBehaviour
{
[SerializeField] Text debugText = null;
// 体力
public int HP = 100;
public int MaxHP = 100;
// 攻撃力
public int Power = 10;
// 状態
public bool isGrounded = false;
public bool isAnimationEnd = true;
public bool isAttackR1 = false;
public bool isAttackR1_2 = false;
public bool isRolling = false;
public bool isJumping = false;
public bool isDied = false;
public void resetStatus()
{
isAnimationEnd = true;
isAttackR1 = false;
isAttackR1_2 = false;
isRolling = false;
isJumping = false;
isDied = false;
}
}
}
まず、namespace を作っています。
体力と攻撃力は後で使います。
状態のところは、Animator で設定したパラメータの変数を管理するためのものです。
resetStatus() で、状態のパラメータをリセットします。
PlayerAnimation.cs
アニメーションに設定したイベントから呼び出す関数と、Animator のパラメータに値を設定するためのクラスです。
アニメーションから関数を呼び出すところは、攻撃を作るところでやります。
using UnityEngine;
namespace Bonfire
{
public class PlayerAnimation : MonoBehaviour
{
Animator animator = null;
PlayerStatus status = null;
//
// Animation のイベントから呼び出す関数
//
void CallAnimationEnd()
{
status.resetStatus();
}
void Start()
{
animator = GetComponent<Animator>();
status = GetComponent<PlayerStatus>();
}
void Update()
{
animator.SetBool("IsGrounded", status.isGrounded);
animator.SetBool("IsAttackR1", status.isAttackR1);
animator.SetBool("IsAttackR1_2", status.isAttackR1_2);
animator.SetBool("IsRolling", status.isRolling);
animator.SetBool("IsJumping", status.isJumping);
}
}
}
CallAnimationEnd()は、アニメーションのイベントから呼び出す関数です。歩かせる&走らせるでは使っていませんが、アニメーションが終了した時にステータスをfalse にするための処理が書いてあります。正確には、アニメーションが終わりそうになって次の入力を受け付けるタイミングを通知するために使っています。
Update 内で Animator のパラメータを設定しています。見ての通りです。基本的にはここで Animator のパラメータ設定しているのですが、Speed と SpeedVertical は PlayerMove.cs で変更してます。
PlayerCtrl.cs
プレイヤーの状態とコントローラからの入力の処理を受け持つクラスです。
[SerializeField] Text debugText = null;
// 入力関連
[SerializeField] float joystickDead = 0.3f;
float inputHorizontalStickL = 0f;
float inputVerticalStickL = 0f;
// 状態管理
State state = State.Walking; // 現在の状態
State nextState = State.Walking; // 次の状態
bool isStateEnd = true;
// 走っている、全力疾走しているフラグ
public bool isRunning = false;
public bool isSprint = false;
// xボタンの入力が、ローリングからダッシュになるまでの時間を計算するための変数
float timeSinceInputFire1 = 0f;
PlayerStatus status = null;
PlayerMove playerMove = null;
まず、変数の定義部分です。基本的には変数名とコメントを見ればわかると思います。
- debugText は、HUDを作ってから使います。
- 走っている、全力疾走しているフラグの isRunning と isSprint は、PlayerMove.cs から参照するため public 宣言してあります。
- timeSinceInputFire1 は、×ボタンを短く押すとローリングorバックステップ。長押しでダッシュという機能を実現するために使用するタイマーです。
void Update()
{
UpdateTimer();
inputHorizontalStickL = Input.GetAxis("Horizontal Stick-L");
inputVerticalStickL = Input.GetAxis("Vertical Stick-L") * -1f; // 上下が逆なので -1 をかけておく
// 呼び出しが多いので、短い文字数で呼べるようにしておく
isStateEnd = status.isAnimationEnd;
- UpdateTimer は、各種タイマー類を進めるためのメソッドです。今は、1つだけですが、後で追加すると思います。
- inputHorizontalStickL と inputVerticalStickL の値は、ここで読んでしまいます。ボタン操作と処理が絡むのと、コントローラーの入力を返すためのメソッドで使うので1回読んで終わりにします。
- ここで inputVerticalStickL に -1 を掛けていますが、Project Setting の Input で設定しても構いません。私の場合、以前書いたように共通のファイルを使っているためこちらで修正しています。
// ボタンの処理は、ここに足していく
// 加速処理
if (Input.GetButton("Fire_1"))
{
isRunning = true;
}
if (Input.GetButtonUp("Fire_1"))
{
isRunning = false;
}
// 全力疾走処理
if (Input.GetButtonDown("Fire_L3"))
{
isSprint = isSprint ? false : true;
}
ボタンの入力処理は、色々な場所に書くことを試してみたのですが、今はここに落ち着いています。ボタン入力のチェック後、次のステートの開始処理と続けて処理することができるためここにしました。
- Fire_1 の処理は、今は、ボタンを押すと走る状態になり、ボタンを離すと走るのをやめるようにする処理だけにしています。後日、短時間で指が離れたらローリングになるような処理を追加します。
- 全力疾走の方は、L3 を1回押すと有効になり、もう1度押すと解除されるようになっています。
// 次の状態に遷移できるようになった時の処理
if (isStateEnd)
{
State prevState = state;
// StartXX 内で nextState が書き換えられるので、ここで state を更新しておく必要がある
state = nextState;
switch (nextState)
{
case State.Walking:
StartWalking(prevState);
break;
case State.AttackR1:
StartAttackR1(prevState);
break;
<中略>
}
}
この部分は、コメントのとおりです。状態が変わるときの開始処理の振り分けが書いてあります。
//
// コントローラーの入力を返すためのメソッド
//
public Vector3 GetPlayerDirection()
{
return Vector3.Normalize(new Vector3(inputHorizontalStickL, 0, inputVerticalStickL));
}
public float GetMagunitudeStickL()
{
float ret = new Vector3(inputHorizontalStickL, 0, inputVerticalStickL).magnitude;
return ret;
}
public bool IsInputStickL()
{
float h = inputHorizontalStickL;
float v = inputVerticalStickL;
bool ret = ((h > joystickDead) || (h < -joystickDead) || (v > joystickDead) || (v < -joystickDead));
return ret;
}
左スティックの処理です。PlayerMove.cs で使います。何となく、入力は1つのクラスでまとめたかったのでこうなりました。
順に、スティックの向き、スティックがどの程度倒れているか、スティックでの入力があるかを返すメソッドです。
//
// 状態(State)を開始するときに行う処理
//
<中略>
private void StartJump(State prevState)
{
StateStartCommon();
status.isJumping = true;
playerMove.AddJumpPower();
}
コメントの通りです。基本的には、PlayerStatus.cs で管理してるパラメータを初期化して、次に移るステータスだけフラグをtrue にします。Jump のように開始時の処理がある場合、ここに書きます。
//
// 共通処理のメソッド
//
private void StateStartCommon()
{
//Debug.Log("StateStartCommon()");
status.resetStatus();
status.isAnimationEnd = false;
nextState = State.Walking;
}
status のリセットをおこないます。これからアニメーションが開始されるので、isAnimationEnd を false にします。また共通で nextState は State.Walking なのでここで書いています。まだ出てきていませんが、nextState が異なるものが出てきた場合は、StateStartCommon() を呼び出した側で設定します。
PlayerMove.cs
プレイヤーの移動を処理するクラスです。このスクリプトは CharacterController コンポーネントを使った場合のスクリプトです。後日、Rigidbody 版も書いてみます。
移動速度の処理
このあたりが、移動速度関連の処理です。
// 移動速度
internal float walkSpeed = 4f;
internal float runSpeedUpRatio = 2.0f;
internal float sprintSpeedUpRatio = 2.0f;
<中略>
float moveSpeed = 0f;
float speedByStick = 0f;
float runSpeedUp = 1.0f;
float sprintSpeedUp = 1.0f;
<中略>
// x ボタンでの加速
if (playerCtrl.isRunning)
{
runSpeedUp = runSpeedUpRatio;
}
else
{
playerCtrl.isSprint = false;
}
// L3 ボタンでのsprint加速
if (playerCtrl.isSprint)
{
sprintSpeedUp = sprintSpeedUpRatio;
}
// スティックの傾きによる速さの変化
speedByStick = playerCtrl.GetMagunitudeStickL();
moveSpeed = speedByStick * walkSpeed * runSpeedUp * sprintSpeedUp;
velocity = transform.forward.normalized * moveSpeed;
<中略>
// アニメーションの速度設定
animator.SetFloat("Speed", moveSpeed);
animator.SetFloat("SpeedVertical", velocity.y);
// CharacterControllerを使って動かす.
characterController.Move(velocity * Time.deltaTime);
中心の処理は以下2行ですね
moveSpeed = speedByStick * walkSpeed * runSpeedUp * sprintSpeedUp;
velocity = transform.forward.normalized * moveSpeed;
移動速度を、スティックの傾き、歩行速度、猛ダッシュのスピードと掛けていっています。
それを、キャラクターの正面方向に進むように velocity に設定しています。
あとは、x ボタンの else の処理ですが、x ボタンを離すと L3の効果がなくなるようにしています。
最後の characterController.Move(velocity * Time.deltaTime) でキャラクターを移動させています。
キャラクターの方向を変える処理
以下の部分です。
// プレイヤーの向き
Vector3 playerDirection = playerCtrl.GetPlayerDirection();
// カメラの向きに、スティックの向きを掛け合わせ、キャラクターをスティックの方向に向かせる
Vector3 cameraDirection = lookTarget.position - mainCamera.transform.position;
cameraDirection.y = 0;
cameraDirection = Vector3.Normalize(cameraDirection);
Quaternion characterTargetRotation = Quaternion.LookRotation(cameraDirection) * Quaternion.LookRotation(playerDirection);
transform.rotation = Quaternion.RotateTowards(transform.rotation, characterTargetRotation, rotationSpeed * Time.deltaTime);
- playerDirection に左スティックの向きを入れる。
- カメラの向きを算出する
- characterTargetRotation としてカメラの向きとプレイヤーの向きを掛け合わせる
- Quaternion.RotateTowards で、キャラクターをなめらかにそっちを向かせる
この部分はどこも参照せずに自分で書いたので、正しいかどうかよく分かっていません。色々やっていたら期待通りの動きになったので、そのまま使っています。いくら読んでも Quaternion が理解できない……。
着地判定
CharacterController に isGrounded があるからそれでいいじゃないかと思っていたのですが、これが全然期待通りに動いてくれないので、着地判定は独自のものを入れています。
きちんと動くときは全然問題ないのですが、ダメな時は isGrounded が true と false を細かく繰り返してプルプルしていたり、全然 true になってくれなかったりします。アニメーションのせいなのか、CharacterController のカプセルの位置の設定が悪いのか原因はよく分かりません。
コードはこの部分です。真下に SphereCast して、当たっているか判定します。
// 独自の着地判定
Ray ray = new Ray(transform.position + 0.18f * transform.up, -transform.up);
isGrounded = Physics.SphereCast(ray, 0.13f, isGroundedTestRayDistance);
status.isGrounded = isGrounded;
LineCast か RayCast を使う方法も見つけたのですが、今一つ精度が悪いので足元にBoxCollider か SphereCollider を設置しようかと思っていました。RigidBody の調べもの中に、このサイトにたどり着きました。
ジャンプ
まだジャンプの話はしていませんが、コードに入れているので簡単に。
// 重力
currentVelocity.y -= GravityPower * Time.deltaTime;
// 速くなりすぎないようにする(速すぎると地面を突き抜けやすくなりそうな気がするため)
currentVelocity.y = Mathf.Max(currentVelocity.y, maxFallSpeed);
// 主にジャンプの着地判定
if (isGrounded && currentVelocity.y < -0.1f && isJumpStarted)
{
status.resetStatus();
isJumpStarted = false;
}
if (isGrounded && !isJumpStarted)
{
// 接地していたら思いっきり地面に押し付ける
currentVelocity.y = -1;
}
// Y 方向の速度を加える
velocity += currentVelocity;
<中略>
public void AddJumpPower()
{
isJumpStarted = true;
currentVelocity.y = jumpPower;
}
- currentVelocity.y -= GravityPower * Time.deltaTime;
色々なものを見ると重力はこう書いているので、マネしているだけです。これを書かなくても Project Settings を開いて、Physics の Gravity を見ると Y に -9.81 が入っているから勝手に落ちる気がします。
なので、2重に重力を書いているような気がします。Physics の Gravity を切る予定があるので、このままにしておきます。 - 空を飛べるようなプロトタイプを作っていた時に、高く飛びすぎると地面を突き抜けていたので、一応落下速度の上限を付けています。もしかしたら、突き抜けていたのは RigidBody の時だったかもしれないです。
- if (isGrounded && currentVelocity.y < -0.1f && isJumpStarted)
怪しげな着地判定です。CurrentVelocity を書かないとジャンプしてくれないので、プロトタイプということで、とりあえずこうしています。 - currentVelocity.y = -1; のところはコメントの通り。勉強した本によると、CharacterController の特性らしいので、そのままマネしています。
- public void AddJumpPower()
これは、 PlayerCtrl.cs から呼び出されます。
Gizmo の設定
以下のメソッドを書いておくと、Gizmo がオンになっていて、かつプレイヤーにフォーカスが当たっているときに、メソッド内のコードを実行してくれます。
// Call by Unity
private void OnDrawGizmosSelected()
{
// 着地判定の範囲を可視化する
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(transform.position + (0.18f - isGroundedTestRayDistance) * transform.up, 0.13f);
}
着地判定の確認用の球体を表示させています。足元の青い球体です。
おわりに
以上がスクリプトの説明です。次回以降、これに追加していく形で説明していきたいと思います。
次回は、デバッグ用HUDを追加します。isGrounded の確認や、現状のステートがどうなっているかを確認するために使えます。