Complete remaining client gap features
This commit is contained in:
+187
-16
@@ -6,17 +6,31 @@
|
||||
# c.world = metin2_world; add_child(c); c.make_current()
|
||||
extends Camera3D
|
||||
|
||||
enum CameraMode {
|
||||
NORMAL,
|
||||
STAND,
|
||||
BLEND,
|
||||
}
|
||||
|
||||
# ClientVS22 stores camera settings in centimetres/degrees. The Godot scene
|
||||
# is metres/radians, so keep the conversion at this boundary instead of
|
||||
# leaking mixed units into the orbit controls.
|
||||
const DEFAULT_DISTANCE_CM := 1550.0
|
||||
const DEFAULT_PITCH_DEG := 27.0
|
||||
const DEFAULT_ROTATION_DEG := 0.0
|
||||
const DEFAULT_HEIGHT_CM := 100.0
|
||||
|
||||
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 yaw := deg_to_rad(DEFAULT_ROTATION_DEG)
|
||||
var pitch := deg_to_rad(DEFAULT_PITCH_DEG)
|
||||
var dist := DEFAULT_DISTANCE_CM * 0.01
|
||||
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 min_dist := 2.0
|
||||
var max_dist := 25.0
|
||||
var sensitivity := 0.006
|
||||
var zoom_step := 1.4
|
||||
var follow_lerp := 12.0
|
||||
@@ -41,6 +55,20 @@ var _key_orbit := 0
|
||||
var _key_zoom := 0
|
||||
var _key_pitch := 0
|
||||
|
||||
# PythonApplicationCamera.cpp's NORMAL / STAND / BLEND modes. The normal
|
||||
# camera follows the player; event cameras use a fixed centre and temporarily
|
||||
# lock the orbit input. `setting` dictionaries intentionally retain the
|
||||
# reference field names so quest scripts can be passed through unchanged.
|
||||
var camera_mode: CameraMode = CameraMode.NORMAL
|
||||
var _event_setting := {}
|
||||
var _default_setting := {}
|
||||
var _blend_from := {}
|
||||
var _blend_to := {}
|
||||
var _blend_elapsed := 0.0
|
||||
var _blend_duration := 0.0
|
||||
var _event_offset := Vector3.ZERO
|
||||
var _event_center := Vector3.ZERO
|
||||
|
||||
const OCCLUDER_MASK := 1 << 1 # 静态遮挡物层(Metin2World 给建筑的盒碰撞)
|
||||
const FADE_ALPHA := 0.72 # 挡住玩家时的透明度
|
||||
var _faded: Array[Node] = [] # 上一帧被淡出的 GeometryInstance3D
|
||||
@@ -48,6 +76,7 @@ var _faded: Array[Node] = [] # 上一帧被淡出的 GeometryInstance3D
|
||||
func _ready() -> void:
|
||||
fov = 55.0
|
||||
far = 4000.0
|
||||
_apply_setting(_normal_setting())
|
||||
if target:
|
||||
global_position = _desired_pos(true)
|
||||
_pos_ready = true
|
||||
@@ -55,12 +84,14 @@ func _ready() -> void:
|
||||
func _unhandled_input(e: InputEvent) -> void:
|
||||
if e is InputEventMouseButton:
|
||||
if e.button_index == MOUSE_BUTTON_RIGHT:
|
||||
_dragging = e.pressed
|
||||
_dragging = e.pressed and not is_event_locked()
|
||||
elif e.button_index == MOUSE_BUTTON_WHEEL_UP and e.pressed:
|
||||
dist = maxf(min_dist, dist - zoom_step)
|
||||
if not is_event_locked():
|
||||
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:
|
||||
if not is_event_locked():
|
||||
dist = minf(max_dist, dist + zoom_step)
|
||||
elif e is InputEventMouseMotion and _dragging and not is_event_locked():
|
||||
yaw -= e.relative.x * sensitivity
|
||||
pitch = clampf(pitch + e.relative.y * sensitivity, min_pitch, max_pitch)
|
||||
elif e is InputEventScreenTouch:
|
||||
@@ -115,6 +146,9 @@ func heading() -> float:
|
||||
# 相机看向的水平方向(供角色移动「相对相机」用)
|
||||
return yaw
|
||||
|
||||
func is_event_locked() -> bool:
|
||||
return camera_mode != CameraMode.NORMAL
|
||||
|
||||
func set_key_orbit(direction: int) -> void:
|
||||
_key_orbit = clampi(direction, -1, 1)
|
||||
|
||||
@@ -125,12 +159,16 @@ 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 head := _look_target()
|
||||
var off := Vector3(
|
||||
sin(yaw) * cos(pitch),
|
||||
sin(pitch),
|
||||
cos(yaw) * cos(pitch)) * dist
|
||||
var want := head + off
|
||||
var want := head + off + _event_offset
|
||||
if is_event_locked():
|
||||
# Event cameras are deliberately fixed shots. The reference event
|
||||
# camera does not chase the actor while a quest is being presented.
|
||||
return want
|
||||
# 地形防穿:沿 head->want 采样,低于地表就把相机拉回
|
||||
if world and world.has_method("sample_height"):
|
||||
var d := want - head
|
||||
@@ -151,6 +189,129 @@ func _desired_pos(snap := false) -> Vector3:
|
||||
want = hit.position - n * 0.3
|
||||
return want
|
||||
|
||||
func _look_target() -> Vector3:
|
||||
if is_event_locked():
|
||||
return _event_center
|
||||
if target:
|
||||
return target.global_position + head_offset
|
||||
return global_position
|
||||
|
||||
func _normal_setting() -> Dictionary:
|
||||
var centre := target.global_position if target else Vector3.ZERO
|
||||
var server := MapCoord.to_server_cm(centre)
|
||||
return {
|
||||
"x": server.x, "y": server.y, "z": centre.y * 100.0,
|
||||
"up": 0.0, "view": 0.0, "cross": 0.0,
|
||||
"distance": dist * 100.0,
|
||||
"rot": rad_to_deg(yaw),
|
||||
"pitch": rad_to_deg(pitch),
|
||||
}
|
||||
|
||||
static func _setting_value(setting: Dictionary, key: String, fallback: float) -> float:
|
||||
if not setting.has(key):
|
||||
return fallback
|
||||
return float(setting[key])
|
||||
|
||||
func _setting_world_center(setting: Dictionary) -> Vector3:
|
||||
# SCameraSetting uses the same x/y ground plane and z-up convention as
|
||||
# server positions. MapCoord is the single source of truth for the map
|
||||
# origin and the Y/Z handedness conversion.
|
||||
var sx := _setting_value(setting, "x", 0.0)
|
||||
var sy := _setting_value(setting, "y", 0.0)
|
||||
var sz := _setting_value(setting, "z", DEFAULT_HEIGHT_CM)
|
||||
return MapCoord.to_world(Vector3(sx * 0.01, sz * 0.01, -sy * 0.01))
|
||||
|
||||
func _runtime_setting(setting: Dictionary) -> Dictionary:
|
||||
var s := setting.duplicate(true)
|
||||
s["_world_center"] = _setting_world_center(s)
|
||||
s["distance"] = clampf(_setting_value(s, "distance", DEFAULT_DISTANCE_CM) * 0.01,
|
||||
min_dist, max_dist)
|
||||
s["pitch"] = clampf(deg_to_rad(_setting_value(s, "pitch", DEFAULT_PITCH_DEG)),
|
||||
min_pitch, max_pitch)
|
||||
s["rot"] = deg_to_rad(_setting_value(s, "rot", DEFAULT_ROTATION_DEG))
|
||||
s["up"] = _setting_value(s, "up", 0.0) * 0.01
|
||||
s["view"] = _setting_value(s, "view", 0.0) * 0.01
|
||||
s["cross"] = _setting_value(s, "cross", 0.0) * 0.01
|
||||
return s
|
||||
|
||||
func _apply_setting(setting: Dictionary) -> void:
|
||||
var s := _runtime_setting(setting)
|
||||
yaw = s["rot"]
|
||||
pitch = s["pitch"]
|
||||
dist = s["distance"]
|
||||
_event_offset = Vector3(s["cross"], s["up"], s["view"])
|
||||
|
||||
func _setting_for_blend() -> Dictionary:
|
||||
if camera_mode == CameraMode.NORMAL:
|
||||
return _normal_setting()
|
||||
return _event_setting.duplicate(true)
|
||||
|
||||
func _lerp_setting(a: Dictionary, b: Dictionary, t: float) -> Dictionary:
|
||||
var out := {}
|
||||
for key in ["x", "y", "z", "up", "view", "cross", "distance", "rot", "pitch"]:
|
||||
out[key] = lerpf(_setting_value(a, key, 0.0), _setting_value(b, key, 0.0), t)
|
||||
return out
|
||||
|
||||
# CPythonApplication::SetEventCamera. The first event preserves the player's
|
||||
# current normal settings so RESTORE_CAMERA returns to the user's view.
|
||||
func set_event_camera(setting: Dictionary) -> void:
|
||||
if camera_mode == CameraMode.NORMAL:
|
||||
_default_setting = _normal_setting()
|
||||
_event_setting = setting.duplicate(true)
|
||||
_event_center = _setting_world_center(_event_setting)
|
||||
_apply_setting(_event_setting)
|
||||
camera_mode = CameraMode.STAND
|
||||
_pos_ready = false
|
||||
|
||||
# CPythonApplication::BlendEventCamera. `blendtime` follows the reference
|
||||
# camera API and is expressed in seconds (the event parser passes it through).
|
||||
func blend_event_camera(setting: Dictionary, blendtime: float) -> void:
|
||||
_blend_from = _setting_for_blend()
|
||||
_blend_to = setting.duplicate(true)
|
||||
_blend_elapsed = 0.0
|
||||
_blend_duration = maxf(0.0, blendtime)
|
||||
_event_setting = _blend_from.duplicate(true)
|
||||
_event_center = _setting_world_center(_blend_from)
|
||||
camera_mode = CameraMode.BLEND
|
||||
_event_offset = Vector3.ZERO
|
||||
_event_center = Vector3.ZERO
|
||||
if _blend_duration <= 0.0:
|
||||
_event_setting = _blend_to.duplicate(true)
|
||||
_event_center = _setting_world_center(_event_setting)
|
||||
_apply_setting(_event_setting)
|
||||
camera_mode = CameraMode.STAND
|
||||
_pos_ready = false
|
||||
|
||||
func set_default_camera() -> void:
|
||||
camera_mode = CameraMode.NORMAL
|
||||
_blend_elapsed = 0.0
|
||||
_blend_duration = 0.0
|
||||
_event_offset = Vector3.ZERO
|
||||
if not _default_setting.is_empty():
|
||||
_apply_setting(_default_setting)
|
||||
_default_setting = {}
|
||||
_event_setting = {}
|
||||
_pos_ready = false
|
||||
|
||||
func get_camera_setting() -> Dictionary:
|
||||
var s := _setting_for_blend()
|
||||
# During BLEND, expose the interpolated frame, matching GetCameraSetting's
|
||||
# use by a subsequent camera event.
|
||||
if camera_mode == CameraMode.BLEND:
|
||||
var t := 1.0 if _blend_duration <= 0.0 else clampf(_blend_elapsed / _blend_duration, 0.0, 1.0)
|
||||
s = _lerp_setting(_blend_from, _blend_to, t)
|
||||
return s
|
||||
|
||||
func snap_to_target() -> void:
|
||||
if target == null:
|
||||
return
|
||||
_pos_ready = false
|
||||
if camera_mode == CameraMode.NORMAL:
|
||||
global_position = _desired_pos(true)
|
||||
else:
|
||||
global_position = _desired_pos(true)
|
||||
look_at(_look_target(), Vector3.UP)
|
||||
|
||||
var _shake := 0.0 # 当前抖动强度(米),指数衰减
|
||||
var _shake_decay := 8.0
|
||||
|
||||
@@ -162,11 +323,20 @@ func shake(strength: float, decay := 8.0) -> void:
|
||||
func _process(dt: float) -> void:
|
||||
if target == null:
|
||||
return
|
||||
if _key_orbit != 0:
|
||||
if camera_mode == CameraMode.BLEND:
|
||||
_blend_elapsed += maxf(0.0, dt)
|
||||
var t := 1.0 if _blend_duration <= 0.0 else clampf(_blend_elapsed / _blend_duration, 0.0, 1.0)
|
||||
_event_setting = _lerp_setting(_blend_from, _blend_to, clampf(t, 0.0, 1.0))
|
||||
_event_center = _setting_world_center(_event_setting)
|
||||
_apply_setting(_event_setting)
|
||||
if t >= 1.0:
|
||||
camera_mode = CameraMode.STAND
|
||||
_event_setting = _blend_to.duplicate(true)
|
||||
if camera_mode == CameraMode.NORMAL and _key_orbit != 0:
|
||||
yaw += float(_key_orbit) * dt * 1.8
|
||||
if _key_zoom != 0:
|
||||
if camera_mode == CameraMode.NORMAL and _key_zoom != 0:
|
||||
dist = clampf(dist + float(_key_zoom) * dt * 5.0, min_dist, max_dist)
|
||||
if _key_pitch != 0:
|
||||
if camera_mode == CameraMode.NORMAL and _key_pitch != 0:
|
||||
pitch = clampf(pitch + float(_key_pitch) * dt * 1.2, min_pitch, max_pitch)
|
||||
var want := _desired_pos()
|
||||
if _pos_ready:
|
||||
@@ -179,8 +349,9 @@ func _process(dt: float) -> void:
|
||||
_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()
|
||||
look_at(_look_target(), Vector3.UP)
|
||||
if camera_mode == CameraMode.NORMAL:
|
||||
_fade_occluders()
|
||||
|
||||
# §8.2 相机与玩家之间的建筑半透明淡出。沿 cam->head 逐段射线,逐个 hit 淡出,
|
||||
# 移过撞点继续;本帧没被挡到的恢复不透明。
|
||||
|
||||
Reference in New Issue
Block a user