Unityでプレイヤーを動かすときに何を使うべきか
2Dと3Dどちらもプレイヤーに相当するオブジェクトを何で動かすべきなのか悩みます。
結論から言うとゲームの雰囲気や特性によって使うものを決めることが大事で、
AddForce
にとらわれないのも大事です。
Qiitaの他の詳しく解説されている記事がありますので参考にしてもらいたいのですが、
AddForceとは結局なんなのかと、問題の回避方法を検証していきます。
本題は AddForceなのですが、それ以外の方法も一応書いておきます。
テトリスなどブロックの操作など物理演算をしなくてもいい場合や空間転移する場合 transform.position
そもそも物理演算をしない(RigidBodyを使わない)ときは、
transform.position += new Vector3(0,1,2)
のように動かします。
初めのうちは物理演算を使う場合でも使ってしまう気持ちもわかりますが、すり抜けたり、オブジェクトが埋まってしまったりします。
なのでやはりおすすめしません。
ただし、何もないところに移動することや、物理法則を無視しても問題ない場合はこれで移動しても良いと思います。
rigidbody.MovePosition
rigidbody.MovePosition(rigidbody.position + new Vector3(0, 1, 2));
こちらも、座標に対して移動する方法ですが、動かす方も当たる方も補間の設定(「補間」にするなど)をすると、途中にある物体にちゃんと当たり判定がされ、高速で衝突します。
private Rigidbody rigidbody;
private bool isSpace = false;
void Start()
{
rigidbody = GetComponent<Rigidbody>();
}
private void FixedUpdate()
{
if (isSpace)
{
rigidbody.MovePosition(new Vector3(0, 100, 0));
isSpace = false;
}
}
// Update is called once per frame
void Update()
{
if (Input.GetKey(KeyCode.Space))
{
isSpace = true;
}
}
例えばこれで実行すると、空中にある (0,10,0)の箱が 押されて (0,110,0)に移動します。
これらについて
ここにわかりやすい検証ページがありますので紹介します。
ちなみに、 rigidbody.MovePosition をFixedUpdate
で呼び出していますが、僕はこのような瞬間的な変更をする場合はUpdateでも良いという認識です。
ただし、Updateの場合は次のゲームフレームまでに物理演算が行われていない可能性がありますので、フレーム単位で物理的な処理がある前提の処理をしたい場合や、キーを押している間中動くといった場合、 例えばフレームレートを固定していても、Updateは1秒間で呼び出される回数が不安定なので、
FixedUpdate
のほうが正確に呼びされますのでその場合は、FixedUpdate
で処理したほうが良いです。
そもそも AddForce とは??
さてようやく本題ですが、よく「力を加える」と言う説明がされていますが、「力を加える」とはなんでしょうか。
Unityの教科書を見ても、「力を加える」、「適当に係数を設定する」みたいな書き方になっています。
(説明がわかっても実際は、摩擦などがあるため適当に設定することになるのと思うのですが)
僕も実は、この記事を見るまであやふやだったのですが、この記事を見てスッキリしました。
https://www.f-sp.com/entry/2016/08/16/211214
この記事から引用させてもらうと
rigidbody.AddForce(Vector3.forward * 0.1f, ForceMode.Force);
は
rigidbody.velocity += (Vector3.forward * 0.1f) * Time.fixedDeltaTime / rigidbody.mass;
と等価というものです。
(もしかしたら、Unityのエンジン内で別に何かあるかもしれませんので、知ってる方はコメントください)
ちなみに、AddForceの第2引数を省略したら、ForceMode.Forceを指定されているのと同じです。
ただし、その瞬間にrigidbody.velocity
が変わるわけではなく次のFixedUpdate
時に反映されます。
AddForceで変わるものを計算してみる
private Rigidbody rigidbody;
void Start()
{
rigidbody = GetComponent<Rigidbody>();
}
private bool isSpace = false;
private bool isLog = false;
private void FixedUpdate()
{
if (isLog)
{
// AddForce後に Updateの前にFixedが呼ばれたら出力
Debug.Log("Fixed");
}
if (isSpace)
{
isSpace = false;
rigidbody.AddForce(new Vector3(0, 100, 0));
Debug.Break();
isLog = true;
}
}
// Update is called once per frame
void Update()
{
isLog = false;
if (Input.GetKeyDown(KeyCode.Space))
{
isSpace = true;
}
}
このようなコードを書いてみました。
ちなみに、rigidbody.AddForce 後に Updateがすぐ来る場合はうまく計算できるが、
もう一度FixedUpdateが呼ばれる可能性もあり、その場合、計算が少しずれるのでその対策のため、Fixedをログに出力しています。
(Fixedが出なかったら実験としては成功)
Debug.Break()
は次のゲームフレームでUnityが一時停止してくれます。
ここで重要なのは座標ではなく Rigidbodyの Info内にある Velocity のYです
1.8038
となっています。なぜ1.8038なのでしょうか。
rigidbody.AddForce(new Vector3(0, 100, 0));
は
rigidbody.velocity += new Vector3(0, 100, 0) * Time.fixedDeltaTime / rigidbody.mass
と等価でした。
ここで Time.fixedDeltaTime
は プロジェクト設定の「固定時間ステップ」なので 0.02です。
rigidbody.mass
は質量なので1です。
よって、
rigidbody.velocity += new Vector3(0, 100, 0) * 0.02f / 1f
なのですが、ここで実は重力の影響があり
実は、重力は常時 AddForce(0 , -9.81f , 0)
されているのと同じなのです。
1固定時間ステップ分影響があるため
rigidbody.velocity += new Vector3(0, 100, 0) * 0.02f/ 1f - new Vector3(0 , -9.81f , 0) * 0.02f
`
y軸だけ見て
100 * 0.02f - 9.81f * 0.02f = 1.8038
と計算通りになりました。
では ForceMode.Impulse は?
ForceMode.Impulse は 力積|Wikipeda のことで、瞬発的に力を加えると説明されていることがあり、実際にそうなんですが、疑問がわきます。
ボタン押した瞬間にForceMode.ImpulseでAddForceを呼び出す場合でも ForceMode.Forceでも 1回しか呼ばれないのに
何が違うんだろうか。
先程のコードでAddForceの引数に ForceMode.Impulseを指定しました。
private Rigidbody rigidbody;
void Start()
{
rigidbody = GetComponent<Rigidbody>();
}
private bool isSpace = false;
private bool isLog = false;
private void FixedUpdate()
{
if (isLog)
{
// AddForce後に Updateの前にFixedが呼ばれたら出力
Debug.Log("Fixed");
}
if (isSpace)
{
isSpace = false;
rigidbody.AddForce(new Vector3(0, 100, 0), ForceMode.Impulse);
Debug.Break();
isLog = true;
}
}
// Update is called once per frame
void Update()
{
isLog = false;
if (Input.GetKeyDown(KeyCode.Space))
{
isSpace = true;
}
}
先程のサイトによると
rigidbody.AddForce(new Vector3(0, 100, 0), ForceMode.Impulse);
は
rigidbody.velocity += new Vector3(0, 100, 0) / rigidbody.mass;
と等価である。
ここでも重力の影響が 1固定時間ステップ分影響があり
100-9.81*0.02 = 99.8038
となります。
ちなみに、質量を2に変えると その分 速度が1/2になりました。
ForceMode.Impulseを指定すると、 ForceMode.Forceに比べて速度が一気に50倍(固定時間ステップが0.02の場合)
されることがわかりました。
逆に言うとそれくらいの違いしかないのはと想像されます。
また
ForceMode.Acceleration
は ForceMode.Force
でのrigidbody.mass
が必ず1として処理され
ForceMode.VelocityChange
は ForceMode.Impulse
での rigidbody.mass
が 必ず1として処理されるものです。
なので、質量が違っても同じ速度として動かす場合はこれらを使うと良いです。
(同じ力でも質量によって動きを変えたかったら ForceMode.Force
か ForceMode.Impulse
)
AddForceとはなんだったのか
AddForceは力を設定することであり、高校物理で習う F = ma
なので 加速度
がでてきますが、 Unityでの加速度というパラメータはなく
a = \frac{\Delta v}{\Delta t}
より
\Delta v = a \Delta t = \frac{F}{m} \Delta t
微小時間に速度の差分を計算することがわかります。
ここで 微小時間は固定時間ステップの0.02秒のことで(正確に言うとリアル時間ではなく、ゲーム内時間) FixedUpdate
が呼ばれそこで速度に加算されます。
つまり、AddForceはRigidBody
内の速度パラメータ(velocity)を変えているだけ(と思われます)で、
AddForceを使わずに自分でvelocityを変更してもいいわけですし、
ForceMode.Impulse
などは、ForceMode.Force
とは、倍率が違うだけと見ることもできます。
(ただし、ForceMode.Impulse
は文字通りに瞬間的なものに使うほうがプログラムの見やすさ的にも良いです)
ということですので原理的にはAddForceを使わなくても自分で計算し、velocityに加算すれば良いはずです。
ただ、毎回重力なども含めすべての力を計算するのも大変なので AddForceが準備されているので、やはりAddForceを使うのが良いのかなと思います。
プレイヤーをAddForceで動かすときにハマること
さて、本題のAddForceを使ってプレイヤーを動かす事を考えます。
よくある教科書などにも書いてあるので、AddForce使って動かすんだなと思うわけなんですが、
private void FixedUpdate()
{
var hori = Input.GetAxis("Horizontal");
var vert = Input.GetAxis("Vertical");
rigidbody.AddForce(new Vector3(hori, 0, vert) * 5);
}
こんな感じで動かしてみたらあれ?と思うことがあると思います。
- 速度が徐々に増える感じになる
- 最大速度を超えてしまう
- 最大速度を超えたら力を加えないようにしても速度が一定ではない
- ドリフト現象になる (ボタンを離したのに滑る)
車のゲームを作る場合は、もちろんこれらでも良いのですが、
人のキャラクターを操作する場合は、違和感が出てしまいます。
改めて考えてみるとキーボードを押したときに、プレイヤーはどうなるべきなんでしょうか。
- その方向に位置を移動させる
- その方向に速度を加える (その方向以外は速度を0にする)
- その方向に力を加える
どれも同じように見えますが別の概念の話になります。
(積分をすれば関係がある概念ですが)
実際どれで実装してもよく、ゲームの相性にあったものをすると良いと思います。
どれもメリット・デメリットがあるのでそれを理解して実装しましょう。さらにあとから変えられるようにしてあるとベストです。
(そのゲームに慣性があってもよいのかや、リアルの動きを求めたいのかなど)
位置を移動させる
private void FixedUpdate()
{
var hori = Input.GetAxis("Horizontal");
var vert = Input.GetAxis("Vertical");
rigidbody.MovePosition(rigidbody.position + new Vector3(hori, 0, vert));
}
ボタンを押したらその分移動するし、離したらすぐ止まります。
これはこれで良いと思います、ただ慣性がまったくないのでそこは違和感が出るかなと思います、
以下GetAxis
でやっていますが GetAxisRaw
でやってもほぼ同様な結果になると思います。
ちなみに
GetAxis
は [-1,1]
の範囲の値を取り、キーボードでキーを入力したときに アナログスティックで入力したような補正がかかり、単純に押したから 1 とはならないようです。アナログスティックの場合は、そのままの傾きです。
GetAxisRaw
は アナログスティックの場合は同じく[-1,1]
の範囲 ですが、キーボードでキーを入力したときに押した場合は補正がかからず -1,0,1 に限定されます。
速度を変える
private void FixedUpdate()
{
var hori = Input.GetAxis("Horizontal");
var vert = Input.GetAxis("Vertical");
rigidbody.velocity = new Vector3(hori, 0, vert) * 10;
}
実は、この方法を使うと
- 速度が徐々に増える感じになる
- 最大速度を超えてしまう
- 最大速度をあたりでなるべく速度を一定化したい
- ドリフト現象になるときがある
が一気に改善できます。
なので、そこまでリアルさを求めないカジュアルなゲームでは十分ありなんだと思います。
ただ、rigidbody.velocity
を書き換えるのは良くないのではという意見もあります。
確かに公式ページを見ても
In most cases you should not modify the velocity directly, as this can result in unrealistic behaviour
とありますが、「リアルな動きにならないから使わないで」ということなので、
ゲームはリアルな動きを求めないほうがゲームとして良い場合もあるので(ジャンプ中に動けるなど)
そういう意味でそこまでリアルを求めずに速度を直接変えてもいいのではという気がします。
だた、欠点としていきなり急ブレーキをかけた感じになることや、摩擦力が違う事がある場合に実装が難しいです。
氷のステージなど滑ってほしいのに直接速度を変えてしまうと滑らなくなります。
(もちろん、自力で判定して、別で処理することもできます。)
やっぱり、地形ごとに合った動きもさせたいし、リアルな動きをさせたいからAddForceを使いたい。なおかつ上の問題も解決していきたいとは思います。
AddForceを使ったときのハマりどころを改善する
まず、AddForceは文字通り「力」なので、本当にその方向に力がかかっているのかは考えないといけないです。
例えば、坂道を横断するして歩く場合、現実でも重力と摩擦を考えると斜めの力がかかっているため、本当にまっすぐだけの力なら、少し滑っていくはずです。
真っ直ぐに進んでいると思っていても少し斜めに力を入れていると思います。
そういう力を表現できるかということになります。
いきなり最高速にする・最高速を超えないようにする・最大速度あたりでもなるべく速度を一定化したい
AddForceの欠点としては、ただ使うだけだと徐々に速度が上がる動きになります。
また最高速度という概念がないので教科書でもよく見られますが
ある速度未満のときだけ力を加えるみたいな実装が見受けられます。
if (rigidbody.velocity.magnitude < 5)
{
rigidbody.AddForce(new Vector3(hori,0,vert)*3000);
}
ちなみにせっかくなので人形のキャラクターとしてUnityChan を使っています。
誤りというわけではなくこれはこれで良いのですが、力を加えたときに最高速を超えてしまい、Colliderの設定にもよりますが、この場合でも最高5.2くらいまで行きました。
(気にしなければ、これはこれで良いと思います。)
次に、キーボードの入力の場合 Vector3(1,0,1) というのがありうるため、
(アナログスティックなら問題ない)
移動ベクトルの大きさが1より多かった場合は 1に正規化する処理をこのようにしました。
var hori = Input.GetAxis("Horizontal");
var vert = Input.GetAxis("Vertical");
var moveVector = new Vector3(hori, 0, vert);
if (moveVector.magnitude > 1)
{
// 大きさが1より大きかったら1に正規化する(主にキーボードのため)
moveVector.Normalize();
}
最大速を5にしたいがアナログスティックの傾きの量を単純に掛けたのを最大速にするとする。
今の速度からその最高速になる分だけ力を加えれば良く、その速度は
(5 * moveVector.magnitude - rigidbody.velocity.magnitude)
です。
これを1固定時間ステップで与えるため 加速度としては fixedDeltaTimeで割る値になります。
(5 * moveVector.magnitude - rigidbody.velocity.magnitude) / Time.fixedDeltaTime
これを係数として 入力のベクトルを1に正規化したものに掛けて与えます。
private void FixedUpdate()
{
var hori = Input.GetAxisRaw("Horizontal");
var vert = Input.GetAxisRaw("Vertical");
var moveVector = new Vector3(hori, 0, vert);
if (moveVector.magnitude > 1)
{
// 大きさが1より大きかったら1に正規化する(主にキーボードのため)
moveVector.Normalize();
}
// 以後 moveVector.magnitudeの大きさは最大でも1
// 最大速はアナログスティックを倒した量分の倍率とし、最大の速度までの力を計算する
var factor = (5 * moveVector.magnitude - rigidbody.velocity.magnitude) / Time.fixedDeltaTime;
rigidbody.AddForce(moveVector * factor);
transform.localRotation = Quaternion.Lerp(transform.localRotation, Quaternion.LookRotation(moveVector),
20.0f * Time.deltaTime);
}
InfoのSpeedがほぼ5になっていると思います。
(摩擦があるため少し下がります。ここを常時5にするのは難しいですし、気になるかもしれませんが微小なので気にする必要もないと思います。)
これで、「いきなり最高速にする」・「最高速を超えないようにする」・「最大速度あたりでもなるべく速度を一定化したい」が解決できます。
ドリフトする挙動を改善する
上の動画では速度はいい感じになったのですが、実は同時押しはしていなくて特に右に動いたあとに下ボタンしか押してないのにを右にひっぱられているような感じになっております。
これを直していきたいわけですが、なぜこうなるかというと
Z方向(下)に力を入れたとしても X方向(右)の速度を0にしていないので
X方向の速度が残ってしまっているというのが原因です。
(間違いやすいのですが、力が残っているわけではありません)
車やボールのゲームなら自然ですが、人のキャラクターだと違和感があります。
自作されているゲームではドリフトみたいな挙動にならないという方は、それはそれで良くてこの問題を考えなくても良いと思います。
ちなみに、なぜX方向(右) の速度が0になるかというと、地面とキャラクターの摩擦の物理演算で速度が少し経つと0になるからです。
摩擦と質量と速度の設定次第では、すぐに0になるのでドリフト現象が出ないかもしれません。
ドリフト現象をどうなってなくすか
結局は、キーを離したときに摩擦で0になりきらず、少し滑るのが原因なので、色々調査しましたが、手っ取り早いのは 地面かキャラクターにつけるColliderのMaterialを「Dynamic Friction」 を1よりもかなり大きな値にすることです。
これは実は、最大値1のように見えて、最大値は無限らしいです。
推奨は 0〜1 のようですが、結局リアルを求めてない挙動なのでいいかなと思います。
1にしてもすぐ止まる摩擦というわけでもないので大きく設定しても良いかなと思いますし、
この摩擦係数を変えると滑りやすい地面も実装できるので良いかなと思います。
参考
なお、摩擦を1より大きく設定したくないという方は このような力にしても良いと思います、FixedUpdateで速度を打ち消す力をいれます。
rigidbody.AddForce(-rigidbody.velocity / Time.fixedDeltaTime);
摩擦力0の地形では
さてここまできて、地形で摩擦力0にしたらこのような動きになります。
スケート選手みたいな動きになるので、ゲームとしては、滑る地形のときはもうひと工夫しないといけないのかなと思います。
(現実を考えると、滑っている最中に他の方向に力を入れられないので、速度が0になったときだけ動けるなどの実装が必要なのかなと思います)
ちなみに、こういう地形でもvelocityに値を直接入れる実装の場合は、
private void FixedUpdate()
{
var hori = Input.GetAxis("Horizontal");
var vert = Input.GetAxis("Vertical");
rigidbody.velocity = new Vector3(hori, 0, vert) * 10;
}
速度が上書きされるので滑らなくなります。
ただ結局、摩擦が0で、スケート選手みたいな動きではなく、速度が0になったときだけ動けるみたいな実装をするのならvelocity
を使うのもAddForce
を使うのも同じ様な実装になると思うので
結局は rigidbody.velocity
に直接入れても良いのではという気になってきました..
ここまで来てですが、プレイヤーを動かすのにAddForce
を使うと良い場面がありましたらぜひ教えて下さい。🙏
まとめ
結局は、この記事通りにするのではなく、自分のゲームにあった方法に工夫してほしいが、AddForce
を理解することによってよくわからないけど使うみたいな怖さを減らして欲しいと思います。
結局の所理解しないままAddForce
を使うとハマることも多いので
物理シミュレーションならともかく、ゲームならrigidbody.velocity
の値を変えてもよいのではと思います。
-
AddForce
はrigidbody.velocity
(速度)の"変化"を記述しているとみなせる - 実質的には
AddForce
のモードの種類は、係数が違うだけとみなせる -
AddForce
を使うなら速度管理が大変 - ゲームの特性によって「座標を変える」のか、「速度を変える」のか、「力を変える」のかを選択する。
左のものほどコントローラーの操作のままになるが、現実の挙動とは少し変わってくる
右のものほど現実の挙動になるが、現実の力のかけ方をよく観察しないと変わったものになる - AddForceを使わずにrigidbody.velocityを直接変更しても良いかもしれない