手掌指针
This commit is contained in:
@@ -31,3 +31,19 @@
|
||||
- `player_pickup.gd` 接入 `throw_carry`(Q 键)→ `_throw()`:取 `-_hold_pos.global_transform.basis.z` 作投掷方向(持握点挂在摄像机下且仅有平移,其 basis 即视线朝向,故无需再注入摄像机),`set_linear_velocity(dir * throw_speed)` 后置 `_current = null`。
|
||||
- 新增导出参数 `throw_speed: float = 12.0`。
|
||||
- 备选手感(注释里留了行):改用 `apply_central_impulse` 则重物飞得慢。
|
||||
|
||||
### 可拖拽时准心变手型
|
||||
- 新增素材 `character/assets/hand_cursor.svg`(新建了 `character/assets/` 目录):简笔手(4 指 + 手掌 + 拇指的圆角矩形),双层 —— 底层深色 `#1a1a1a` stroke-width 4 当描边,上层白色 stroke-width 1.5 覆盖内部接缝。32×32,**未写 `<title>`**(避免 ThorVG 解析风险)。
|
||||
- `ui/crosshair.gd`:加 `class_name Crosshair`;新增 `hand_texture`(自动加载上面的 svg) / `hand_size`(=26)、`set_grabbable(bool)`;`_draw()` 在 `_grabbable && hand_texture != null` 时走 `_draw_hand()`,否则走 `_draw_dot()`。
|
||||
- `player_pickup.gd`:新增 `signal hover_changed(grabbable: bool)`,`physics_step()` 里先调 `_update_hover()`——用 `_ray.force_raycast_update()` 强制刷新(否则结果滞后于本帧 move_and_slide,跑动中瞄到物体慢一帧),**只在状态变化时**发信号。
|
||||
- `player.gd`:`@onready var crosshair: Crosshair = $CameraRig/HUD/Crosshair`;`_ready()` 权威分支里 `pickup.hover_changed.connect(_on_pickup_hover_changed)`;handler 里既切准心,也 `Input.set_custom_mouse_cursor()` 换系统指针(含指尖热点),鼠标未捕获时该行无副作用。
|
||||
- **注意**:用户侧已把 `AnimationPlayer` 从 `Body/AnimationPlayer` 提到 Player 根节点(`$AnimationPlayer`),并把 `animator` 变量改名为 `animation_player`;相机 scale 改为 5。
|
||||
|
||||
### 换用真实光标素材(grab / grab_hover)
|
||||
- 素材来源:`C:\Users\yvmou\Downloads\cursors\light\`(light 主题 = 深色手型,32×32 RGBA)。
|
||||
- 已复制到 `character/assets/grab.png`(指着可拖拽物)与 `character/assets/grab_hover.png`(正抓着),Godot 已生成 `.import`。
|
||||
- **删除了临时的 `character/assets/hand_cursor.svg` 及其 `.import`**(已被真实素材取代)。
|
||||
- `ui/crosshair.gd` 改为三态:`enum GrabState { NONE, HOVER, HOLDING }`;导出 `hover_texture` / `hold_texture` / `hand_size`(=32,1:1 不缩放) / `icon_modulate` / `sync_os_cursor`;公开 `texture_for_state(state)` 供系统指针复用素材;`_sync_os_cursor()` 内部完成 `Input.set_custom_mouse_cursor`(`Engine.is_editor_hint()` 下跳过),热点在图标高度 12% 处 —— **系统指针逻辑已从 player.gd 移入准心**。
|
||||
- `player_pickup.gd`:信号由 `hover_changed(bool)` 改为 `interact_state_changed(hovering, holding)`,`_update_hover()` 改名 `_update_interact_state()` 并同时跟踪 `_holding`。
|
||||
- `player.gd`:handler 改名 `_on_pickup_interact_state_changed(false)`,只负责把两个布尔量收敛成 `Crosshair.GrabState`(优先级 抓着 > 指着 > 没有)。
|
||||
- 待用户确认:像素手在非整数倍分辨率缩放下发糊的话,需把 Crosshair 的 `texture_filter` 设为 Nearest。
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c67qe66m2aft4"
|
||||
path="res://.godot/imported/grab.png-15ccc11b1ce7bc67c3d60db6d0ffc1ed.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://character/assets/grab.png"
|
||||
dest_files=["res://.godot/imported/grab.png-15ccc11b1ce7bc67c3d60db6d0ffc1ed.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 341 B |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bkq6ya350cae8"
|
||||
path="res://.godot/imported/grab_hover.png-ae4fc79bd64160559a632cc7a661f9e3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://character/assets/grab_hover.png"
|
||||
dest_files=["res://.godot/imported/grab_hover.png-ae4fc79bd64160559a632cc7a661f9e3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -5,13 +5,15 @@ extends CharacterBody3D
|
||||
## Look 视角 character/player/player_look.gd
|
||||
## Motor 移动跳跃 character/player/player_motor.gd
|
||||
## Pickup 拾取 character/player/player_pickup.gd
|
||||
## Animator 动画表现 character/player/player_animator.gd(挂在 Body/AnimationPlayer 上)
|
||||
## Animator 动画表现 character/player/player_animator.gd(挂在 AnimationPlayer 上)
|
||||
##
|
||||
## 本脚本不实现任何操作,只负责「谁先谁后」和「谁有权跑」。
|
||||
|
||||
@onready var body: Node3D = $Body
|
||||
@onready var camera_rig: Node3D = $CameraRig
|
||||
@onready var camera: Camera3D = $CameraRig/Camera
|
||||
## 准心(挂在 CameraRig/HUD 下)。远端玩家的 HUD 会随 camera_rig 一起被释放。
|
||||
@onready var crosshair: Crosshair = $CameraRig/HUD/Crosshair
|
||||
@onready var animation_player: PlayerAnimator = $AnimationPlayer
|
||||
@onready var look: PlayerLook = $Look
|
||||
@onready var motor: PlayerMotor = $Motor
|
||||
@@ -43,6 +45,8 @@ func _ready() -> void:
|
||||
camera.get_node("InteractionRay") as RayCast3D,
|
||||
camera.get_node("InteractionPos") as Marker3D
|
||||
)
|
||||
# 准心接线:拾取组件一发现交互状态变化,就通知准心换外观(点 / 手 / 抓)。
|
||||
pickup.interact_state_changed.connect(_on_pickup_interact_state_changed)
|
||||
set_mouse_captured(true)
|
||||
|
||||
|
||||
@@ -62,6 +66,17 @@ func set_mouse_captured(captured: bool) -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if captured else Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
|
||||
## 把拾取组件的两个布尔量收敛成准心的三态。优先级:抓着 > 指着 > 没有。
|
||||
## 系统鼠标指针由准心自己同步(Crosshair.sync_os_cursor),这里不用管。
|
||||
func _on_pickup_interact_state_changed(hovering: bool, holding: bool) -> void:
|
||||
var state := Crosshair.GrabState.NONE
|
||||
if holding:
|
||||
state = Crosshair.GrabState.HOLDING
|
||||
elif hovering:
|
||||
state = Crosshair.GrabState.HOVER
|
||||
crosshair.set_grab_state(state)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# 顺序固定:算速度 → 推进 → 播动画 → 搬物体。
|
||||
# 由根节点统一编排,各组件自己不许写 _physics_process ——
|
||||
|
||||
@@ -12,9 +12,17 @@ extends Node
|
||||
## 投掷初速度(米/秒):按 Q 甩出去时给物体的速度大小。
|
||||
@export var throw_speed: float = 12.0
|
||||
|
||||
## 交互状态变化时发出。
|
||||
## hovering = 准心正指着可拖拽的刚体(还没抓);holding = 正抓着东西。
|
||||
## Player 接住这个信号去切准心外观。只在两者之一变化时发,避免每帧刷 UI。
|
||||
signal interact_state_changed(hovering: bool, holding: bool)
|
||||
|
||||
var _current: RigidBody3D = null
|
||||
var _ray: RayCast3D
|
||||
var _hold_pos: Marker3D
|
||||
## 上一次发出的状态,用来只在「变化那一刻」发信号。
|
||||
var _hovering: bool = false
|
||||
var _holding: bool = false
|
||||
|
||||
|
||||
## 由 Player 在 _ready() 注入依赖。
|
||||
@@ -42,6 +50,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
## 用速度把物体拽向持握点,而不是直接改 global_position ——
|
||||
## 这样它仍会和场景里其他刚体正常碰撞,不会被硬塞进墙里。
|
||||
func physics_step() -> void:
|
||||
_update_interact_state()
|
||||
# 用 is_instance_valid 而不是 `== null`:物体可能在拖拽途中被 destroy,
|
||||
# 那种情况下引用不是 null 而是「已被释放」,直接取 global_position 会报错。
|
||||
if not is_instance_valid(_current) or not is_instance_valid(_hold_pos):
|
||||
@@ -50,6 +59,23 @@ func physics_step() -> void:
|
||||
_current.set_linear_velocity(to_hold * pick_power)
|
||||
|
||||
|
||||
## 检测「指着可拖拽物 / 正抓着东西」这两个状态,**只在变化时**发信号。
|
||||
## 每帧都发会让 UI 无谓重绘,所以这里自己记着上一次的结果。
|
||||
func _update_interact_state() -> void:
|
||||
if _ray == null:
|
||||
return
|
||||
# 主动刷新一次:RayCast3D 的结果通常滞后于本帧的 move_and_slide,
|
||||
# 不强制更新的话,跑动中瞄到物体要慢一帧才亮。
|
||||
_ray.force_raycast_update()
|
||||
var hovering := _ray.get_collider() is RigidBody3D
|
||||
var holding := is_instance_valid(_current)
|
||||
if hovering == _hovering and holding == _holding:
|
||||
return
|
||||
_hovering = hovering
|
||||
_holding = holding
|
||||
interact_state_changed.emit(_hovering, _holding)
|
||||
|
||||
|
||||
func _grab() -> void:
|
||||
if _ray == null:
|
||||
return
|
||||
|
||||
+108
-2
@@ -1,11 +1,31 @@
|
||||
@tool
|
||||
class_name Crosshair
|
||||
extends Control
|
||||
## 简易准心:屏幕正中央的一个点,用 _draw() 实时绘制,不依赖任何贴图。
|
||||
## 准心 / 指针:屏幕正中央。
|
||||
## 三种状态:
|
||||
## NONE 没指着可拖拽物 → 画一个点
|
||||
## HOVER 指着可拖拽物 → 显示 grab.png(张开的手,提示「可以抓」)
|
||||
## HOLDING 正抓着东西 → 显示 grab_hover.png(抓取态)
|
||||
##
|
||||
## 挂法:CanvasLayer > Crosshair(Control),Control 的锚点设为「Full Rect」
|
||||
## (anchor 全 0/1),这样 size 就等于视口尺寸,中心即屏幕中心。
|
||||
## 因为是 CanvasLayer 子节点,在 window/stretch/mode=canvas_items 下会自动跟随缩放。
|
||||
##
|
||||
## 状态由 Player 驱动:拾取组件的 interact_state_changed 信号 → set_grab_state()。
|
||||
|
||||
## 指着可拖拽物体时的图标。没在 Inspector 里指定时自动加载。
|
||||
const HOVER_TEXTURE_PATH := "res://character/assets/grab_hover.png"
|
||||
## 正抓着东西时的图标。没在 Inspector 里指定时自动加载。
|
||||
const HOLD_TEXTURE_PATH := "res://character/assets/grab.png"
|
||||
|
||||
## 准心的三种状态。
|
||||
enum GrabState {
|
||||
NONE, ## 没指着可拖拽物体
|
||||
HOVER, ## 指着可拖拽物体(还没抓)
|
||||
HOLDING, ## 正抓着东西
|
||||
}
|
||||
|
||||
@export_group("点")
|
||||
## 点的颜色
|
||||
@export var dot_color: Color = Color(1, 1, 1, 0.9):
|
||||
set(v):
|
||||
@@ -18,7 +38,37 @@ extends Control
|
||||
dot_radius = v
|
||||
queue_redraw()
|
||||
|
||||
## 是否给点画一圈描边(浅色背景上也看得清)
|
||||
@export_group("手型")
|
||||
## 指着可拖拽物体时的图标。留空则自动加载 grab.png
|
||||
@export var hover_texture: Texture2D:
|
||||
set(v):
|
||||
hover_texture = v
|
||||
queue_redraw()
|
||||
|
||||
## 正抓着东西时的图标。留空则自动加载 grab_hover.png
|
||||
@export var hold_texture: Texture2D:
|
||||
set(v):
|
||||
hold_texture = v
|
||||
queue_redraw()
|
||||
|
||||
## 图标显示的边长(像素)。素材是 32×32,默认 32 就是 1:1 不缩放,像素最清晰。
|
||||
@export_range(8.0, 96.0, 1.0) var hand_size: float = 32.0:
|
||||
set(v):
|
||||
hand_size = v
|
||||
queue_redraw()
|
||||
|
||||
## 图标着色。素材自带颜色,默认 Color.WHITE 表示按原样显示(乘法调色)。
|
||||
@export var icon_modulate: Color = Color(1, 1, 1, 1):
|
||||
set(v):
|
||||
icon_modulate = v
|
||||
queue_redraw()
|
||||
|
||||
## 是否把同一张图同步给系统鼠标指针。
|
||||
## 游戏内鼠标是 MOUSE_MODE_CAPTURED 锁定隐藏的,这行只在鼠标可见时(Esc / 菜单)才有可见效果。
|
||||
@export var sync_os_cursor: bool = true
|
||||
|
||||
@export_group("描边")
|
||||
## 是否给点画描边(浅色背景上也看得清)
|
||||
@export var outline: bool = true:
|
||||
set(v):
|
||||
outline = v
|
||||
@@ -36,10 +86,17 @@ extends Control
|
||||
outline_width = v
|
||||
queue_redraw()
|
||||
|
||||
## 当前状态。
|
||||
var _state: int = GrabState.NONE
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# 准心不吃鼠标事件,否则会挡住后面的 UI / 拖拽
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if hover_texture == null and ResourceLoader.exists(HOVER_TEXTURE_PATH):
|
||||
hover_texture = load(HOVER_TEXTURE_PATH)
|
||||
if hold_texture == null and ResourceLoader.exists(HOLD_TEXTURE_PATH):
|
||||
hold_texture = load(HOLD_TEXTURE_PATH)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -49,9 +106,58 @@ func _notification(what: int) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## 由 Player 调用,切换准心状态(GrabState)。
|
||||
func set_grab_state(state: int) -> void:
|
||||
if state == _state:
|
||||
return
|
||||
_state = state
|
||||
if sync_os_cursor and not Engine.is_editor_hint():
|
||||
_sync_os_cursor()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## 某个状态该用哪张图。公开出来是为了让系统指针复用同一份素材,不必两处各加载一遍。
|
||||
func texture_for_state(state: int) -> Texture2D:
|
||||
match state:
|
||||
GrabState.HOVER:
|
||||
return hover_texture if hover_texture != null else hold_texture
|
||||
GrabState.HOLDING:
|
||||
return hold_texture if hold_texture != null else hover_texture
|
||||
_:
|
||||
return null
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var c := size * 0.5 # 控件中心 = 屏幕中心
|
||||
|
||||
var tex := texture_for_state(_state)
|
||||
if tex != null:
|
||||
_draw_icon(c, tex)
|
||||
else:
|
||||
_draw_dot(c)
|
||||
|
||||
|
||||
## 一个点(可选描边)
|
||||
func _draw_dot(c: Vector2) -> void:
|
||||
if outline and outline_width > 0.0:
|
||||
draw_circle(c, dot_radius + outline_width, outline_color)
|
||||
draw_circle(c, dot_radius, dot_color)
|
||||
|
||||
|
||||
## 手型图标:居中、按原始宽高比缩放(非方形素材不会被拉变形)。
|
||||
func _draw_icon(c: Vector2, tex: Texture2D) -> void:
|
||||
var tex_size := Vector2(tex.get_width(), tex.get_height())
|
||||
var k := hand_size / maxf(tex_size.x, tex_size.y)
|
||||
var draw_size := tex_size * k
|
||||
draw_texture_rect(tex, Rect2(c - draw_size * 0.5, draw_size), false, icon_modulate)
|
||||
|
||||
|
||||
## 把当前状态的图标同步给系统鼠标指针。热点放在指尖附近,
|
||||
## 这样「点下去」的位置落在手指上,而不是图标左上角。
|
||||
func _sync_os_cursor() -> void:
|
||||
var tex := texture_for_state(_state)
|
||||
if tex == null:
|
||||
Input.set_custom_mouse_cursor(null, Input.CURSOR_ARROW)
|
||||
return
|
||||
var hotspot := Vector2(tex.get_width() * 0.5, tex.get_height() * 0.12)
|
||||
Input.set_custom_mouse_cursor(tex, Input.CURSOR_ARROW, hotspot)
|
||||
|
||||
Reference in New Issue
Block a user