俺に解るように説明する "Godot Engine 3.x" 入門+

ゲームエンジン Godot Engine に関すること。入門とか使い方とかチュートリアルとか、あれとかこれとか。日本語解説。

Godot キャラコン 02 「カメラぐるぐる 1/2」...横

カプセル移動の前に、カメラ制御をする。結論から言えば、キネマティックボディーの子にヘッドを作成、作ったヘッドの子にカメラをちょっと離して固定。スクリプトでヘッドを動かすと、つられてカメラも動く。こんだけ~~! どんだけ簡単なんだっつーの。ま、カメラを固定しちまってイイのかちょっと疑問だけど、今回はこれで。
f:id:ore2wakaru:20180328124204g:plain


ノードを3つ追加する

ヘッド部分と将来的にカメラを置く部分を作る。いきなりカメラを持ってくるんじゃなくて、ちゃんと動くことを先に確認するため、本物のカメラはまだくっつけない。代わりにダミーのメッシュを置く。将来的には消すけどデバッグ用に、ヘッド用メッシュとカメラに見立てたメッシュを置くってこと。

具体的に追加する3つのノードと解説は、以下。
f:id:ore2wakaru:20180328040919p:plain

[1]

  • 「Spatial」 ノードを追加し、適当にリネーム。くれぐれも変なノードの子にしないでね。
  • で、ここは、マウスが動くと、"3rd Person"カメラがここを支点にぐるぐる回るようにする場所。だから、本当はヘッドと言うよりカメラピボットと言った方がいいね、"3rd Person"の場合は。
  • カメラピボットは、頭のてっぺんにするので、Translationで、y方向に"0.9"してある。(そんな所に人間の目はないぞ! とお考えの方は、お好みで)
    f:id:ore2wakaru:20180328033537p:plain

[2]

  • 「MeshInstance」
  • これは、マウスが動いたとき、思った通りにちゃんとピボットも動いているか確認するためのメッシュ。「Spatial」 ノードは見えねーからな。こんな風に、適当なメッシュを貼り付けて見た目で確認できるようにするんだ。
  • 大きさや形は好きにすればいいと思うけど。メッシュはキューブ、1辺50cmの立方体にしてみた。
    f:id:ore2wakaru:20180328035451p:plain
    しつこいようだけど、メッシュの整形は、上図の四角く囲った図の部分をクリックね。
    f:id:ore2wakaru:20180328035737p:plain

[3]

  • 「MeshInstance」
  • カメラ位置。確認用に置いたもの。
  • z方向にずらす必要があるが、何m動かすかはお好みで。俺はとりあえず5mにした。
    f:id:ore2wakaru:20180328042657p:plain
    俺は、直径50cmの球にしたけど、形も大きさもお好みで。どーせ後で消すし。お寿司。
    f:id:ore2wakaru:20180328043020p:plain


実際のカメラ(「Camera」ノード)は、10mとかそこいら離して置いてね。近すぎるとダミーメッシュの動きが確認できねーからな。


首振りスクリプト

第1段階として、マウスの動きに合わせてヘッドが左右に動くようにしよう。これが出来てから、第2段階として上下に動くようにする。

スクリプト、ずばり、どん。で、見ればわかるから、解説なんかいらないと思うけど、簡潔に。
f:id:ore2wakaru:20180328065425p:plain

[1]

スクリプト「KinematicBody」ノード("KinematicBody_Player")に貼る。マウスはヘッド("Spatial_Head")の動きを制御するだけだから、ヘッドにスクリプトを貼ってもいい。

その場合、移動用のスクリプト"KinematicBody_Player"に、カメラ制御用のスクリプト"Spatial_Head"にと、分けて書くことになる。分けたぶん、それぞれのスクリプトの中身が単純化し、短く書け、間違って変な所をいじってワケが分からなくなる様な事も無くなるだろう。変数だって余計なものは入れないから、スッキリしてイイ。エラーの発見も早いよね。

って、良い事だらけじゃねーか! 分けろや! って思うじゃん。その通り。だから、本当は2つに分けるべきだと思う・・・。だがしかし、、、1個にしちゃったんだな。まーいいじゃん。

[2]

var my_mou_sen = 0.01            # mouse sensitivity

マウスセンシの設定。これが無いと、マウスをちょっと動かしただけでビュンビュン首を振ることになる。ここの数値は、シーンを再生し、実際にマウスを動かしてみて、後で設定し直してみてくれ。

[3]

var my_mou_rel = Vector2(0, 0)   # mouse relative

マウスが動いた分の数値を入れる変数。マウスの動きは前後左右の2次元なので、Vector2型。詳しくは、後で。

[4]

var my_head                      # head node

今回は、ヘッドという他ノードを制御するので、変数を作っておく。これで、スクリプトが横に長くなることを防げる。また、多分、処理速度も速くなる。(注意:速度に関してはウソかも。)

変数の値は_ready()関数のボディで入れているけど、どうしてもここで入れたい場合は、onready varを使って、

onready var my_head = get_node("Spatial_Head")

とすればいいんじゃない。varonready varの違いって前やったしな。ココ:Godot Engine GDScript 11 「神シーン常駐システム完成」...1/2 - 俺に解るように説明する "Godot Engine 3.x" 入門+

[5]

func _ready():
    my_head = get_node("Spatial_Head")

1行目

_ready()関数も前やったけど、これはシーンのノードがシーンツリーに全部入ったら呼ばれる関数ね。今回取ってきたい"Spatial_Head"は、スクリプトを貼るノードの子でしょ。だから、もし"Spatial_Head"がシーンツリーに入ってなかったら、get_node()出来ないわけよ。無いものは取ってこれないってこと。だよね。

「子ノードもシーンツリーに入りましたよ。get_node()も出来ますよ。全ノード、準備OK(ready)ですよー。」っていう時の合図が、_ready()なのね。なんでもかんでも「初期化する時使う」で覚えちゃダメだぜ、Godotの場合は。

2行目

それから、超面白いのが、こうすると、以降、変数"my_head"をごにょごにょすると、"Spatial_Head"本体が操作されるようになるって事。

ここでの変数への代入は、単に一時的な現状データを変数に渡しているだけではナイんだ。なんでか知らないけど、いいか、以降、ノード本体を操作できちゃう様にしちゃったよって事なんだよ! すごいな。便利、便利。便利だけど、不思議やね~。

ノードを変数に代入すると、以降、変数でそのノードを操作できるって事な。だけど、やっぱ不思議やわ~。

[6]

func _input(event):

新しいバーチャル関数だな。これ_input(event)関数は、見たまんま、インプットがあったら呼ばれる関数。マウスの動きも感知。

キーボードからの入力は、Inputクラスが保存してくれるようだから、前やったように

    if Input.is_action_pressed("うんたら~"):
        かんたら~

みたいに、_input(event)関数を呼ばずに出来るんだけど、どうもマウスの動き(モーション)は、上手いこと使えるようなデータとして、Inputクラスには保存してないみたいだ。(本当は何かあるのかもな~。)

だから、イイ感じのデータを引き出すために、わざわざこの関数を呼び出してるってわけ。めんどいな。マウスの動きを取ってくる時は、func _input(event):で、とでも覚えておこう。

[7]

    if event is InputEventMouseMotion:
        my_mou_rel = event.relative

で、もっとめんどいのが、ここ。

1行目は、「もし インプットイベントがマウスの動き型 だったら」という条件が書かれた部。

  • ifはいいだろ。
  • eventは上で出てきた_input(event)のカッコの中のeventな。インプットイベントがどんなものなのかという情報が入ってる。
  • isはちょっと分からん。多分、型(type)を比べる時に使うキーワード。==は数値が同じかどうかを比べる時、で、isは型が同じかどうかを比べる時に使う感じか? 今回は、数値(具体的にはマウスが動いた距離)を比べてるわけではないので、==は使えないってことか? 違うかも。
  • InputEventMouseMotionは、数あるインプットイベント型のひとつ。マウスの動き型。マウスが動くと、どうもコイツが持ってる変数にデータが入るようだ。
  • あと、インプットイベント型、API見ればいっぱいあるのが分かるよね。
    f:id:ore2wakaru:20180328103646p:plain

こんな感じで、さっき言ったみたいに、「もし インプットイベントがマウスの動き型 だったら」という風になる。もっと簡単にするなら、「もしマウスが動いたら」でいいよね。

2行目は、「もしマウスが動いてたら」実行される部分で、「前回マウスがあった場所からどんだけ動いたか、その数値を"my_mou_rel"に入れろ。」ってこと。

  • "my_mou_rel"はいいだろ。自分で作った変数な。
  • event.relativeはあれだ。まず、relativeInputEventMouseMotionAPIで見ると分かるんだけど、InputEventMouseMotionは2つ変数を持っていて、その1つ。簡単に言うと、常に「マウスが動いた距離」が入ってくる。(前いた場所からの相対位置が・・・英語読んでくれ・・・)  f:id:ore2wakaru:20180328111207p:plain
    しかも、ご丁寧にVector2型だってことも書いてくれてるじゃん。だから最初に、"my_mou_rel"Vector2型で初期値を入れてたってわけ。
  • で、eventは、InputEventMouseMotionだったろ。つまり、event.relativeInputEventMouseMotion.relativeって事だよ。
  • ちなみにrelative(発:レラティブ、意:相対の)。

ま、ちょっと分かんない所もあるけど、こうやってマウスがどんだけ動いたかのデータを引き出すんだなって事がつかめればイイんじゃない。やや納得?

もしかしたら、2行目、

        my_mou_rel += event.relative

かもな?

[8]

func _physics_process(delta):

_physics_process(delta)は固定タイミングで呼び出されるバーチャルファンクション。プロジェクトセッティングからタイミングを変えられるけど、デフォでは1秒間に60回、等間隔で呼び出してくれる。

基本的に物理モノを扱うときはこの関数内で行うようだ。プレイヤは「KinematicBody」で、物理モノだからイイだろ? でもカメラは物理モノじゃないよね。だから、本当はここじゃなくてもいいんだ。けど、カメラとプレイヤの動きが同期してないとカクカクするらしいので、この関数のボディでカメラの動きもやってしまう感じ。

[9]

    # head movement
    my_head.global_rotate(Vector3(0, 1, 0), -my_mou_rel.x * my_mou_sen)
    my_mou_rel = Vector2(0, 0)

1行目はコメント。

2行目

  • global_rotate()は前やったよね。グローバルで回すってやつね。global_rotate(軸, 角度)ってやって、パラメータに、軸と角度を入れて使う。ヘッドを回したいからこれを使う。すごく当然。
  • まわすものは、"my_head"で変数になってるが、さっきも言った通り、これで本体が回る。恐るべし。
  • パラメータ部分。軸はイイだろ。ちょっと注意なのは角度の部分。マウスの動いた距離を"my_mou_rel"に入れたけど、首を振る時に必要なのは、マウスの移動距離だけ。それを取り出すには、".x"を付けて、"my_mou_rel.x"とする。
    移動データは"my_mou_rel.x"だし、移動データは"my_mou_rel.y"となる。

Vector2型のデータの取り出し方は、例えば、

var my_vec = Vector2(5, 3) 

とかやったら、"my_vec.x""5""my_vec.y""3"ね。こんな風にデータを個別に取り出せる。

  • あと、"my_mou_rel.x""-"(マイナス)が付いてるのは、マウスの移動方向と反対に回したいから。

3行目

  • そして最後の所で、xもyも”0”にしてるけど、これは、マウスが動かなかった場合、前回動いた値がずっと有効になってしまうから、それをここで防いでいる。


確認してみる

では、シーンを再生してみてくれたまえ。マウスの左右の動きに合わせてヘッドが動き、それにつられてダミーカメラのメッシュが動くのが確認できたかな? マウスはゲーム画面の中にないとダメみたいだけどな。

ふむふむ、そうか。

では、次回は縦に動くようにしていこう。


コピペ用

extends KinematicBody

# mouse
var my_mou_sen = 0.01           # mouse sensitivity
var my_mou_rel = Vector2(0, 0)  # mouse relative

# head (camera pivot)
var my_head                     # head node

func _ready():
    my_head = get_node("Spatial_Head")

func _input(event):
    if event is InputEventMouseMotion:
        my_mou_rel = event.relative

func _physics_process(delta):
    
    # head movement
    my_head.global_rotate(Vector3(0, 1, 0), -my_mou_rel.x * my_mou_sen)
    my_mou_rel = Vector2(0, 0)