Files
agame/character/player/face/face_rig.gd
T

163 lines
6.2 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.
class_name FaceRig
extends Node
## 角色的脸:把 mesh 上那个「face」surface 换成自定义材质,
## 材质把「预设底图」和「玩家自绘的笔画层」合成起来。
##
## 笔画层是一张常驻的 Image(不是 SubViewport)—— 这样即使绘画面板没打开,
## 游戏内的脸也一直是玩家画的那张。绘画面板只是把 painter.texture 显示出来,
## 把鼠标坐标换算好之后喂给 painter。
const SHADER_PATH := "res://character/player/face/face_paint.gdshader"
## 存档:选的是哪张预设
const SAVE_PRESET := "user://face_preset.json"
## 存档:笔画层(透明底 PNG)
const SAVE_PAINT := "user://face_paint.png"
## 联机同步:非本机玩家主动去要脸的次数与间隔。
## 各端玩家节点出现的先后顺序不保证,头几次请求可能打空,所以要重试。
const FACE_SYNC_TRIES := 5
const FACE_SYNC_INTERVAL := 0.8
## 第一次要之前先等一会儿。实测:主机生成玩家比客户端早几帧,立刻要会打在一个
## 还没建出来的节点上(Godot 报 "Node not found: Main/<id>/FaceRig"),白刷一堆红字。
const FACE_SYNC_INITIAL_DELAY := 1.0
@export_group("接线")
## 面部所在的 MeshInstance3D 的路径(相对本节点)。默认就是 mouse.glb 里的 body_mesh。
## 存路径而不是节点引用:节点引用是场景实例化时由 Godot 解析的,实测这里解析不到(拿到 null),
## 自己在 _ready() 里 get_node_or_null 才稳。
@export var mesh_path: NodePath = ^"../Body/mouse/MouseRig/Skeleton3D/body_mesh"
## 面部是第几个 surface。mouse.glb 里是 1,名字就叫 face
@export var surface_index: int = 1
## 没有存档时用哪张脸
@export var default_preset: Texture2D
@export_group("画布")
## 画布边长(像素)。玩家画的笔画坐标就在这个空间里
@export var canvas_size: int = 512
@export_group("面部 UV")
## 面部孤岛在贴图上的起点(实测自模型)。模型重导后 UV 变了就改这里
@export var face_uv_min: Vector2 = Vector2(0.142, 0.031)
## 孤岛的边长(UV 单位)
@export var face_uv_size: float = 0.214
## 当前预设的 res:// 路径。存档靠它记住「你选的是哪张脸」
var preset_path: String = ""
## 联机时有没有收到过这张脸主人的数据(收到了就不用再重试)
var _got_face := false
var mesh: MeshInstance3D
var material: ShaderMaterial
var painter: FacePainter
func _ready() -> void:
mesh = get_node_or_null(mesh_path) as MeshInstance3D
if mesh == null:
push_warning("%s 找不到面部 mesh(%s),脸不会有变化" % [get_path(), mesh_path])
return
painter = FacePainter.new(canvas_size)
painter.name = "FacePainter"
add_child(painter)
material = ShaderMaterial.new()
material.shader = load(SHADER_PATH)
material.set_shader_parameter("face_uv_min", face_uv_min)
material.set_shader_parameter("face_uv_size", face_uv_size)
material.set_shader_parameter("paint_tex", painter.texture)
mesh.set_surface_override_material(surface_index, material)
apply_saved()
# 联机时非本机玩家去要脸。单机(OfflineMultiplayerPeer)不用同步
var peer := multiplayer.multiplayer_peer
if peer != null and not (peer is OfflineMultiplayerPeer) and not is_multiplayer_authority():
_pull_remote_face()
# ---------------- 对外接口 ----------------
## 换成某张预设脸。只换底图,玩家画的笔画保留 —— 所以可以边挑预设边留着笔迹。
func set_preset(tex: Texture2D, path: String = "") -> void:
if material != null:
material.set_shader_parameter("base_tex", tex)
if not path.is_empty():
preset_path = path
func clear_paint() -> void:
if painter != null:
painter.clear()
## 存:预设路径 + 笔画层。下次进游戏自动读回来
func save() -> void:
if painter == null:
return
painter.save_png(SAVE_PAINT)
var f := FileAccess.open(SAVE_PRESET, FileAccess.WRITE)
if f == null:
push_warning("脸部预设存不下来:%s" % SAVE_PRESET)
return
f.store_string(JSON.stringify({"preset": preset_path}))
## 读存档;没有(或存的预设已经不在了)就退回 default_preset
func apply_saved() -> void:
var preset: Texture2D = default_preset
var path := default_preset.resource_path if default_preset != null else ""
if FileAccess.file_exists(SAVE_PRESET):
var f := FileAccess.open(SAVE_PRESET, FileAccess.READ)
var data: Variant = JSON.parse_string(f.get_as_text()) if f != null else null
if data is Dictionary:
var saved := str(data.get("preset", ""))
if not saved.is_empty() and ResourceLoader.exists(saved):
preset = load(saved)
path = saved
set_preset(preset, path)
if painter != null:
painter.load_png(SAVE_PAINT)
# ---------------- 联机同步 ----------------
# 用「谁缺谁来要」而不是「出生时推」:各端玩家节点出现的先后顺序不保证,
# 推的话很可能推给一个还不存在的节点;而请求总是从已经存在的那一端发出去的。
## 非本机玩家在 _ready 里调,去要这张脸主人的那一份。带重试,因为对端节点可能还没建好。
func _pull_remote_face() -> void:
await get_tree().create_timer(FACE_SYNC_INITIAL_DELAY).timeout
for i in FACE_SYNC_TRIES:
if not is_inside_tree() or _got_face:
return
_rpc_request_face.rpc_id(get_multiplayer_authority())
await get_tree().create_timer(FACE_SYNC_INTERVAL).timeout
@rpc("any_peer", "call_remote", "reliable")
func _rpc_request_face() -> void:
# 只有这张脸的主人回应 —— 别人的脸不该由我来发
if not is_multiplayer_authority():
return
_rpc_send_face.rpc_id(
multiplayer.get_remote_sender_id(),
preset_path,
painter.to_png_bytes() if painter != null else PackedByteArray(),
)
@rpc("any_peer", "call_remote", "reliable")
func _rpc_send_face(preset: String, png: PackedByteArray) -> void:
# 只认这张脸主人的回应;本机自己的脸不接受覆盖
if is_multiplayer_authority() or multiplayer.get_remote_sender_id() != get_multiplayer_authority():
return
_got_face = true
if not preset.is_empty() and ResourceLoader.exists(preset):
set_preset(load(preset), preset)
if png.size() > 0 and painter != null:
var img := Image.new()
if img.load_png_from_buffer(png) == OK:
painter.set_image(img)