Files
mtgodot-poc/project/game_camera.gd
T

214 lines
7.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GameCamera —— Metin2 风第三人称轨道相机。
# 桌面:右键拖拽 -> 环绕(yaw/pitch 滚轮 -> 缩放
# 触屏(F7):单指拖拽 -> 环绕 双指捏合 -> 缩放
# 跟随 target,地形/建筑防穿。
# 用法:var c = preload("res://game_camera.gd").new(); c.target = player;
# c.world = metin2_world; add_child(c); c.make_current()
extends Camera3D
var target: Node3D
var world: Node # Metin2World,用 sample_height 防穿(可空)
var head_offset := Vector3(0, 1.15, 0)
var yaw := 0.7
var pitch := 0.55
var dist := 8.0
var min_pitch := deg_to_rad(12.0)
var max_pitch := deg_to_rad(78.0)
var min_dist := 3.5
var max_dist := 20.0
var sensitivity := 0.006
var zoom_step := 1.4
var follow_lerp := 12.0
# F7 触屏手势
var touch_sensitivity := 0.005 # 单指拖拽 -> yaw/pitch
var pinch_zoom_gain := 0.03 # 捏合像素差 -> dist 变化
const _ORBIT_DEADZONE := 8.0 # 单指移动超过这么多像素才算「拖拽环绕」(低于视作点击)
var _dragging := false
var _pos_ready := false
var _touches := {} # index:int -> position:Vector2(当前按下的手指)
var _orbit_touch := -1 # 正在环绕的手指 index,-1 = 无
var _orbit_active := false # 已越过 deadzone
var _orbit_press := Vector2.ZERO
var _pinch_last := -1.0 # 上一帧两指间距,<0 = 未在捏合
# ClientVS22 game.py drives these while Q/E/R/F/T/G are held. Keeping the
# state here (instead of applying one large step per key event) gives the
# same continuous desktop-camera motion on macOS and Windows.
var _key_orbit := 0
var _key_zoom := 0
var _key_pitch := 0
const OCCLUDER_MASK := 1 << 1 # 静态遮挡物层(Metin2World 给建筑的盒碰撞)
const FADE_ALPHA := 0.72 # 挡住玩家时的透明度
var _faded: Array[Node] = [] # 上一帧被淡出的 GeometryInstance3D
func _ready() -> void:
fov = 55.0
far = 4000.0
if target:
global_position = _desired_pos(true)
_pos_ready = true
func _unhandled_input(e: InputEvent) -> void:
if e is InputEventMouseButton:
if e.button_index == MOUSE_BUTTON_RIGHT:
_dragging = e.pressed
elif e.button_index == MOUSE_BUTTON_WHEEL_UP and e.pressed:
dist = maxf(min_dist, dist - zoom_step)
elif e.button_index == MOUSE_BUTTON_WHEEL_DOWN and e.pressed:
dist = minf(max_dist, dist + zoom_step)
elif e is InputEventMouseMotion and _dragging:
yaw -= e.relative.x * sensitivity
pitch = clampf(pitch + e.relative.y * sensitivity, min_pitch, max_pitch)
elif e is InputEventScreenTouch:
_on_touch(e)
elif e is InputEventScreenDrag:
_on_drag(e)
func _on_touch(e: InputEventScreenTouch) -> void:
if e.pressed:
_touches[e.index] = e.position
if _touches.size() == 1:
_orbit_touch = e.index
_orbit_active = false
_orbit_press = e.position
elif _touches.size() == 2:
_orbit_touch = -1 # 第二指落下 -> 转捏合,取消环绕
_pinch_last = _pinch_dist()
else:
_touches.erase(e.index)
if _touches.size() < 2:
_pinch_last = -1.0
if _touches.size() == 1:
_orbit_touch = _touches.keys()[0] # 抬起一指后,剩下的接管环绕
_orbit_active = false
_orbit_press = _touches[_orbit_touch]
elif _touches.is_empty():
_orbit_touch = -1
func _on_drag(e: InputEventScreenDrag) -> void:
if _touches.has(e.index):
_touches[e.index] = e.position
if _touches.size() >= 2:
var d := _pinch_dist()
if _pinch_last > 0.0:
# 两指张开(d 变大)-> 拉近;捏拢 -> 推远
dist = clampf(dist + (_pinch_last - d) * pinch_zoom_gain, min_dist, max_dist)
_pinch_last = d
elif e.index == _orbit_touch:
if not _orbit_active and e.position.distance_to(_orbit_press) > _ORBIT_DEADZONE:
_orbit_active = true
if _orbit_active:
yaw -= e.relative.x * touch_sensitivity
pitch = clampf(pitch + e.relative.y * touch_sensitivity, min_pitch, max_pitch)
func _pinch_dist() -> float:
var ks := _touches.keys()
if ks.size() < 2:
return 0.0
return (_touches[ks[0]] as Vector2).distance_to(_touches[ks[1]] as Vector2)
func heading() -> float:
# 相机看向的水平方向(供角色移动「相对相机」用)
return yaw
func set_key_orbit(direction: int) -> void:
_key_orbit = clampi(direction, -1, 1)
func set_key_zoom(direction: int) -> void:
_key_zoom = clampi(direction, -1, 1)
func set_key_pitch(direction: int) -> void:
_key_pitch = clampi(direction, -1, 1)
func _desired_pos(snap := false) -> Vector3:
var head := target.global_position + head_offset
var off := Vector3(
sin(yaw) * cos(pitch),
sin(pitch),
cos(yaw) * cos(pitch)) * dist
var want := head + off
# 地形防穿:沿 head->want 采样,低于地表就把相机拉回
if world and world.has_method("sample_height"):
var d := want - head
var steps := 8
for i in range(1, steps + 1):
var p := head + d * (float(i) / steps)
var gy: float = world.call("sample_height", p.x, p.z) + 0.4
if p.y < gy:
want = head + d * (float(i - 1) / steps)
break
# §8.3 建筑防穿:head->want 射线撞静态遮挡物就把相机拉到撞点前
var world3d := get_world_3d()
if world3d:
var q := PhysicsRayQueryParameters3D.create(head, want, OCCLUDER_MASK)
var hit := world3d.direct_space_state.intersect_ray(q)
if hit:
var n := (want - head).normalized()
want = hit.position - n * 0.3
return want
var _shake := 0.0 # 当前抖动强度(米),指数衰减
var _shake_decay := 8.0
# P4:受击 / 暴击轻抖。strength 米,decay 越大越快停。
func shake(strength: float, decay := 8.0) -> void:
_shake = maxf(_shake, strength)
_shake_decay = decay
func _process(dt: float) -> void:
if target == null:
return
if _key_orbit != 0:
yaw += float(_key_orbit) * dt * 1.8
if _key_zoom != 0:
dist = clampf(dist + float(_key_zoom) * dt * 5.0, min_dist, max_dist)
if _key_pitch != 0:
pitch = clampf(pitch + float(_key_pitch) * dt * 1.2, min_pitch, max_pitch)
var want := _desired_pos()
if _pos_ready:
global_position = global_position.lerp(want, clampf(follow_lerp * dt, 0.0, 1.0))
else:
global_position = want
_pos_ready = true
if _shake > 0.001:
global_position += Vector3(randf_range(-1, 1), randf_range(-1, 1), randf_range(-1, 1)) * _shake
_shake = lerpf(_shake, 0.0, clampf(_shake_decay * dt, 0.0, 1.0))
else:
_shake = 0.0
look_at(target.global_position + head_offset, Vector3.UP)
_fade_occluders()
# §8.2 相机与玩家之间的建筑半透明淡出。沿 cam->head 逐段射线,逐个 hit 淡出,
# 移过撞点继续;本帧没被挡到的恢复不透明。
func _fade_occluders() -> void:
var world3d := get_world_3d()
if world3d == null:
return
var head := target.global_position + head_offset
var from := global_position
var this_frame: Array[Node] = []
var ss := world3d.direct_space_state
var exclude: Array[RID] = []
for _i in 4:
var q := PhysicsRayQueryParameters3D.create(from, head, OCCLUDER_MASK)
q.exclude = exclude
var hit := ss.intersect_ray(q)
if hit.is_empty():
break
exclude.append(hit.rid)
# StaticBody3D 上的 meta 指向它的 MeshInstance3DMetin2World 设的)
var body: Object = hit.collider
var gi: Node = body.get_meta("occ_mesh", null) if body and body.has_meta("occ_mesh") else null
if gi and gi is GeometryInstance3D and not this_frame.has(gi):
(gi as GeometryInstance3D).transparency = FADE_ALPHA
this_frame.append(gi)
from = hit.position + (head - from).normalized() * 0.05
for g in _faded:
if is_instance_valid(g) and not this_frame.has(g):
g.transparency = 0.0
_faded = this_frame