手掌指针

This commit is contained in:
2026-10-09 15:28:00 +08:00
parent bbb0c5db14
commit bc88fef43a
8 changed files with 246 additions and 3 deletions
+108 -2
View File
@@ -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)