@tool class_name Crosshair extends Control ## 准心 / 指针:屏幕正中央。 ## 三种状态: ## 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): dot_color = v queue_redraw() ## 点的半径(像素) @export_range(0.5, 12.0, 0.5) var dot_radius: float = 2.5: set(v): 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 queue_redraw() ## 描边颜色 @export var outline_color: Color = Color(0, 0, 0, 0.6): set(v): outline_color = v queue_redraw() ## 描边宽度(像素) @export_range(0.5, 4.0, 0.5) var outline_width: float = 1.0: set(v): 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() func _notification(what: int) -> void: # 窗口尺寸变化时重画(stretch 模式下实时跟着变) if what == NOTIFICATION_RESIZED: 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)