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

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

Godot キャラコン 08 「障害物とカメラ 簡単1本RayCast」

現在カメラは単にヘッドの子。だから、プレイヤとカメラの間に障害物が挟まっても容赦なしに、障害物の裏に回って映してしまう。簡単に言うと、カメラは壁や床の裏に回っちゃうって事。
f:id:ore2wakaru:20180412213721g:plain
そんな所に行っちゃイヤじゃん。ということで、プレイヤとカメラの間に障害物があった場合、カメラが障害物の前に出るように修正する。ただ今回は、RayCast1本の簡易バージョン。


ステージ設置

当たり前だが、床と壁が必要。「スタティックぼでー魔法の組み合わせ」で作ればいいよ。
f:id:ore2wakaru:20180412200947p:plain


RayCast

RayCast(レイキャスト)と言うのは読んで字のごとく、Ray(光線)をCast(投げる)こと。ただ光線を出すだけじゃなくて、飛んでる間に何かにぶち当たったら、その位置やら何やらの情報を取り出せる優れもの。

今回は、何かにさえぎられた場所よりちょっと前にカメラを持ってくるため、ヘッドからカメラ方向にレイを飛ばす。カメラから出してもダメね。


ノードの追加と修正

f:id:ore2wakaru:20180412203511p:plain

[1]

RayCast」ノードをヘッドの子として追加。

[2]

カメラに見立てたダミーメッシュを残していたら、カメラの子に移動。(影が出ちゃうから消した方がいいかも。または非表示で。)


RayCastのプロパティ

f:id:ore2wakaru:20180412210320p:plain

[1]

RayCast」はデフォルトのままでは、レイを飛ばしてくれないので、インスペクタ上で"On"にしておく。

[2]

飛ばす方向と距離をここで設定。ローカル。「RayCast」はヘッドの子。ヘッドが動けば一緒につられて動くのヘッドのローカル方向と考えてもいい。で、カメラは+z方向にあるので"(0, 0, 8)"z方向に8m飛ばすよう設定した。

だがスクリプト上で、カメラは8m先に置くのではなく、そのちょっと手前(1m前)に置くようにしている。これは、レイが当たった場所にぴったんこカメラを持ってくるとオブジェクトの角度により、かなりの場合に、透けてあっちが見えてしまう為。(まー、1m手前にしても結構透ける場合あるけど、今回はこれで。)


スクリプトに追加

f:id:ore2wakaru:20180412211016p:plain

L13

  • ヘッドを動かした時と同じように、他ノードを操作する時は、そのノードを変数にする。(キャッシュとか言うのかな? よく知らんけど)
  • これは「RayCast」ノード用。

L14

  • 同上。こちらは「Camera」ノード用。

L15

  • ヘッドとカメラの標準距離を指定。先ほども述べたが、レイは8m飛ばすが、実際のカメラ位置はそれより1m手前に置く。つまり7mとなる。

f:id:ore2wakaru:20180412211953p:plain

L28~29

  • ヘッド同様に、get_node()して変数に代入。これで他ノードを操作できる。操作っつっても動かすだけじゃなくて、データをゲットしたりセットしたりすることね。

f:id:ore2wakaru:20180412212336p:plain

(見切れてる所あるけど、上部は追加・変更なし。)

f:id:ore2wakaru:20180412212922p:plain

(なんだか文字がちっちゃくなっちゃったな。)

L71

  • is_colliding()RayCastクラスの関数。目標の位置まで飛ばずに途中で何かにコライドしたら、つまりレイが何かに遮られたら"true"を返す。

L72

  • 壁でも床でもいいけど、コリジョンに引っかかったらここね。
  • やってることは、カメラのグローバル位置レイが何かに当たった位置からカメラが向いている方向へ1m前進させている。
  • "my_cam.global_transform.origin": global_transformは前やった様にグローバルで考えろよってこと。originはオブジェクトの中心ってこと。つまり全部でカメラのグローバル位置
  • "my_ray.get_collision_point()": get_collision_point()レイが当たったグローバル位置を返す。
  • "my_cam.global_transform.basis.z": "global_transform.basis"は前やったまんま、オブジェクト自身の軸をグローバルで考えたときどうなるかってやつ。で、".z"が付いてるからz軸だけ考えろって事。しかも、ノーマライズされているから距離としてはピッタリ1m。これでカメラが向いているのと反対の方向(カメラは-z方向を映すからね)に1m行くベクトルが取れる。マイナスしてるから、カメラが向いている方向へ1mということ。
  • さっきも書いたけど、1mずらしてるのは、壁が透けないように。
  • レイが当たった場所はグローバルで返って来るから、カメラ位置もグローバルで考たワケ。

L74

  • コリジョンに引っかからなかったら、ココ。
  • 本来カメラが居てほしい距離まで飛んだら、何もしなくていいかと思いきや、何もせずにほっておくと、場合によってはとても短い距離にカメラが固定されてしまう事がある。なので、いちいち位置を標準距離に設定する。
  • translationはローカル位置。これをヘッドからみてz方向に7mだから、"Vector3(0, 0, my_cam_dis)"となる。
  • こっちは簡単にローカルで考えればいい。
  • ちなみに、"my_cam.global_transform.origin"でグローバル位置が、"my_cam.translation"でローカル位置が、それぞれ求められる。


あとちょっと

これだけやれば行けそうなんだけど、いけないんだわ。なんとレイが「KinematicBody」のカプセルコリジョンに当たることがある。

デフォルトで「RayCast」は親ノードに当たるのは無視してくれるが、親の親(つまり、おじいちゃんorおばあちゃん)ノードを無視してくれない。なんて不親切!

ということで、「KinematicBody」のカプセルコリジョンを無視する方法なんだが、layer (レイヤ)mask(マスク)で処理してみた。


レイヤとマスク

レイヤってのはお絵かきソフトのレイヤと同じで、自分のいる空間ってこと。マスクってのは当たり判定の調査対象レイヤのこと。

だから、RayCastの調査対象をステージを構成している床や壁にして、プレイヤを外せばイイということ。こうすればいい。

[1]

RayCastのマスクは、1番のままにしておく。

f:id:ore2wakaru:20180412234232p:plain

[2]

プレイヤはRayCastの調査対象から外したいので、2番目のレイヤに移動させる。(ただし、プレイヤも壁や床に当たり判定を持ちたいので、マスクは1番のママ。)

f:id:ore2wakaru:20180412233751p:plain

これで、RayCastの調査対象はステージを構成している床や壁(レイヤの1番)にして行われるが、プレイヤには行われなくなる。


結果

f:id:ore2wakaru:20180412195829g:plain

こんな感じで壁の前にカメラが出て、壁が透けなければOK。ただし、判定がレイ1本と簡易なので鋭角すぎると、やっぱり透ける時がある。

もっとうまい事したかったら、カメラの4隅に向けて4本のレイを飛ばすか、カメラにコリジョンを付けて(カメラをキネマティックボディーの子にして)追っかけカメラにするかだな。


こぴぺっぺ

extends KinematicBody

# マウス
var my_mou_sen = 0.003          # マウスセンシ (mouse sensitivity)
var my_mou_rel = Vector2(0, 0)  # マウスが動いた差分 (mouse relative)

# ヘッド (カメラピボット)
var my_head                     # ヘッドノード入れる用
var my_head_ang = 0.0           # ヘッドのうなづき角度(ラジアン) (head angle)
var my_head_ang_max = PI / 3.0  # ヘッドのうなづき限界角度(60°) (head angle MAX)

# カメラ
var my_ray                      # RayCastノード入れる用
var my_cam                      # カメラノード入れる用
var my_cam_dis = 7.0            # ヘッドとカメラまでの標準距離 (camera distance)

# プレイヤ
var my_dir = Vector3(0, 0, 0)   # キー入力での移動方向 (direction)
var my_vel = Vector3(0, 0, 0)   # 目標移動地点 (velocity)
var my_spd = 10.0               # 移動速度 秒速10m (run speed)
var my_acl = 0.03               # 加速率 (acceleration)
var my_dcl = 0.08               # 減速率 (deceleration)
var my_gra = -10.0              # 重力 (gravity)
var my_yyy = 0.0                # 現在のy方向の大きさ保管用

func _ready():
    my_head = get_node("Spatial_Head")
    my_ray  = get_node("Spatial_Head/RayCast")
    my_cam  = get_node("Spatial_Head/Camera_Main")

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

func _physics_process(delta):
    
    # プレイヤ動かす
    # キー入力により移動方向をセットする
    my_dir = Vector3(0, 0, 0)
    if Input.is_action_pressed("my_forward"):
        my_dir = my_dir - my_head.global_transform.basis.z
    if Input.is_action_pressed("my_backward"):
        my_dir = my_dir + my_head.global_transform.basis.z
    if Input.is_action_pressed("my_right"):
        my_dir = my_dir + my_head.global_transform.basis.x
    if Input.is_action_pressed("my_left"):
        my_dir = my_dir - my_head.global_transform.basis.x
    my_dir.y = 0.0
    my_dir = my_dir.normalized()
    # 加速・減速
    if my_dir == Vector3(0, 0, 0):
        my_vel = my_vel.linear_interpolate(Vector3(0, 0, 0), my_dcl)
    else:
        my_vel = my_vel.linear_interpolate(my_dir * my_spd, my_acl)
    # 重力
    my_vel.y = my_yyy + my_gra * delta
    # 移動とy保持
    my_yyy = move_and_slide(my_vel).y
        
    # ヘッド動かす
    # 横回転
    my_head.global_rotate(Vector3(0, 1, 0), -my_mou_rel.x * my_mou_sen)
    # 縦回転
    my_head_ang = my_head_ang - my_mou_rel.y * my_mou_sen
    my_head_ang = clamp(my_head_ang, -my_head_ang_max, my_head_ang_max)
    my_head.rotation.x = my_head_ang
    # マウス移動がない時に勝手に動かない様、0に
    my_mou_rel = Vector2(0, 0)
    
    # 障害物に当たってたらカメラを前に、当たってなければ標準距離に
    if my_ray.is_colliding():
        my_cam.global_transform.origin = my_ray.get_collision_point() - my_cam.global_transform.basis.z
    else:
        my_cam.translation = Vector3(0, 0, my_cam_dis)