重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
godot-2d-physics by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-2d-physics关于 Godot 2D 中碰撞检测、触发器和射线投射的专家指南。
CollisionShape2D 节点 — 使用编辑器中的形状手柄,而不是 Node2D 的缩放属性。缩放会导致不可预测的物理行为和错误的碰撞法线 [12]。collision_layer 和 collision_mask — 图层 = "我是什么?",遮罩 = "我检测什么?"。将两者设置为相同的值通常是错误的 [13]。move_and_slide() 时绝对不要将速度乘以 delta — move_and_slide() 会自动包含时间步长。只需将重力/加速度乘以 delta [14]。force_raycast_update() — 射线投射每物理帧更新一次。如果你改变了 target_position,必须强制更新 [15]。get_overlapping_bodies() — 这很消耗资源。应使用 / 信号来缓存结果 [16]。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
body_enteredbody_exited_process 中直接修改 RigidBody2D 的状态 — 使用 _integrate_forces() 来安全、同步地访问 PhysicsDirectBodyState2D [17, 411]。_process() 中移动 PhysicsBody2D 节点 — 使用 _physics_process()。在物理步骤之外移动物体会导致卡顿和不可靠的碰撞检测。RigidBody2D — 使用 PhysicsServer2D 来绕过节点开销,以获得巨大的性能提升(群体/子弹)[18, 397]。Area2D 用于高频阻挡(子弹) — Area 信号可能会有延迟。使用 move_and_collide() 或 ShapeCast2D 以获得帧精确的结果 [19]。set_deferred — 它会在帧结束时执行。应使用 force_raycast_update() 或 PhysicsServer2D。PhysicsServer2D 的 RID — 它们不会被垃圾回收,会导致永久内存泄漏。强制要求:在实现之前,请阅读与您用例匹配的脚本。
使用命名的图层常量和调试可视化进行程序化的图层/遮罩管理。
针对 PhysicsDirectSpaceState2D 查询的基于帧的缓存 - 消除冗余的昂贵查询。
CharacterBody2D 的自定义物理集成模式。涵盖非标准重力、力和手动步进。用于非标准物理行为。
PhysicsDirectSpaceState2D 查询模式,用于射线投射、点查询和形状查询。用于视线检测、地面检测或区域扫描。
用于成千上万个移动物体的低级 PhysicsServer2D 用法。绕过节点开销,在弹幕或群体模拟中获得巨大的性能提升。
用于高速射弹的手动物理子步进。确保移动速度超过物理刻度的物体具有帧精确的碰撞。
使用 _integrate_forces 进行线程安全的 RigidBody2D 修改。非常适合传送物体或应用自定义冲量而不产生抖动。
使用 PhysicsDirectSpaceState2D 进行轻量级环境感知。执行射线查询,无需 RayCast2D 节点的开销。
使用位运算枚举和辅助函数管理复杂碰撞图层/遮罩的清晰架构模式。
为 AI 优化的多投射视觉系统。复用单个 RayCast2D 在一个物理帧内检查多个角度。
使用 ShapeCast2D 进行稳健的范围效果检测。提供即时碰撞信息,没有 Area2D 的信号延迟。
用于局部重力区域(水、太空、风)和手动角色重量模拟的逻辑。
防止多形状物体进入触发器时信号滥发的专家模式。
标准配置和运行时调整,以确保在高刷新率显示器上角色移动流畅。
直接管理 PhysicsServer2D RID,用于大规模物理模拟的峰值性能。
用于精度关键移动的专家级反弹和摩擦力逻辑实现。
用于防止子弹在极高速度下穿隧的高级 CCD 管理。
使用骑手感知逻辑对多个静态/可动画物体进行优化的批量移动。
# collision_layer (32 位): 我在哪些广播频道上传输?
# collision_mask (32 位): 我在监听哪些广播频道?
# 示例:玩家 vs 敌人
# 玩家:
# layer = 0b0001 (频道 1: "我是玩家")
# mask = 0b0110 (频道 2+3: "我监听敌人和墙壁")
# 敌人:
# layer = 0b0010 (频道 2: "我是敌人")
# mask = 0b0101 (频道 1+3: "我监听玩家和墙壁")
# ✅ 良好做法:使用辅助函数以提高清晰度
func setup_player_collision() -> void:
# 我是图层 1
set_collision_layer_value(1, true)
# 我检测图层 2(敌人)和 3(世界)
set_collision_mask_value(2, true)
set_collision_mask_value(3, true)
# ✅ 良好做法:使用位移进行程序化的图层运算
func enable_layers(base_layer: int, count: int) -> void:
var mask := 0
for i in range(count):
mask |= (1 << (base_layer + i - 1))
collision_mask = mask
# ❌ 不良做法:没有文档说明的硬编码位掩码
collision_mask = 0b110110 # 这是什么意思?!
# 模式:击中敌人但忽略其他射弹的射弹
# projectile.gd
extends Area2D
func _ready() -> void:
set_collision_layer_value(4, true) # 图层 4: "射弹"
set_collision_mask_value(2, true) # 遮罩图层 2: "敌人"
# 结果:射弹之间不会相互碰撞
# 模式:单向平台(玩家可以从下方跳上去)
# platform.gd
extends StaticBody2D
@export var one_way := true
func _ready() -> void:
set_collision_layer_value(3, true) # 图层 3: "世界"
if one_way:
# 使用 Area2D + 碰撞豁免代替
# (标准的单向平台使用不同的技术)
pass
# ❌ 不良做法:如果 Area2D 有多个形状,body_entered 会触发多次
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
print("进入!") # 如果 Area 有 3 个 CollisionShapes,会触发 3 次!
# ✅ 良好做法:使用 Set 跟踪唯一物体
extends Area2D
var _active_bodies := {} # 使用字典作为 Set
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body not in _active_bodies:
_active_bodies[body] = true
print("首次进入!") # 只触发一次
func _on_body_exited(body: Node2D) -> void:
_active_bodies.erase(body)
# lava_zone.gd
extends Area2D
@export var damage_per_tick := 5
@export var tick_rate := 0.5 # 每 0.5 秒造成伤害
var _damage_timers := {} # body -> time_until_next_tick
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body.has_method("take_damage"):
_damage_timers[body] = 0.0 # 立即第一次伤害
func _on_body_exited(body: Node2D) -> void:
_damage_timers.erase(body)
func _process(delta: float) -> void:
for body in _damage_timers.keys():
_damage_timers[body] -= delta
if _damage_timers[body] <= 0.0:
body.take_damage(damage_per_tick)
_damage_timers[body] = tick_rate
# enemy_vision.gd - 敌人看向玩家
extends CharacterBody2D
@onready var vision_ray: RayCast2D = $VisionRay
func can_see_target(target: Node2D) -> bool:
var direction := global_position.direction_to(target.global_position)
vision_ray.target_position = direction * 300 # 300 像素范围
vision_ray.force_raycast_update() # 关键:在帧中更新
if vision_ray.is_colliding():
return vision_ray.get_collider() == target
return false
# platformer_controller.gd
extends CharacterBody2D
@onready var floor_front: RayCast2D = $FloorCheckFront
@onready var floor_back: RayCast2D = $FloorCheckBack
func at_ledge() -> bool:
return floor_front.is_colliding() and not floor_back.is_colliding()
func _physics_process(delta: float) -> void:
if at_ledge() and is_on_floor():
# 敌人 AI:在边缘转身
velocity.x *= -1
# 忽略特定物体(例如,自身)
func _ready() -> void:
$RayCast2D.add_exception(self)
$RayCast2D.add_exception($Weapon) # 忽略附加的武器碰撞体
# 重置排除项
$RayCast2D.clear_exceptions()
# 检查鼠标点击是否击中任何物理物体
func get_body_at_mouse() -> Node2D:
var mouse_pos := get_global_mouse_position()
var space := get_world_2d().direct_space_state
var query := PhysicsPointQueryParameters2D.new()
query.position = mouse_pos
query.collide_with_areas = false
query.collision_mask = 0b11111111 # 所有图层
var results := space.intersect_point(query, 1) # 最多 1 个结果
if results.is_empty():
return null
return results[0].collider
# 玩家周围圆形范围内的伤害
func damage_nearby_enemies(center: Vector2, radius: float, damage: int) -> void:
var space := get_world_2d().direct_space_state
var query := PhysicsShapeQueryParameters2D.new()
var circle := CircleShape2D.new()
circle.radius = radius
query.shape = circle
query.transform = Transform2D(0.0, center)
query.collision_mask = 0b0010 # 图层 2: 敌人
var hits := space.intersect_shape(query)
for hit in hits:
var enemy: Node2D = hit.collider
if enemy.has_method("take_damage"):
enemy.take_damage(damage)
# 命中扫描武器(无射弹)
func fire_hitscan_weapon(from: Vector2, direction: Vector2, max_range: float) -> void:
var space := get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(from, from + direction * max_range)
query.exclude = [self]
query.collision_mask = 0b0010 # 敌人
var result := space.intersect_ray(query)
if result:
var hit_enemy: Node2D = result.collider
var hit_point: Vector2 = result.position
spawn_hit_effect(hit_point)
if hit_enemy.has_method("take_damage"):
hit_enemy.take_damage(25)
| 用例 | 方法 | 原因 |
|---|---|---|
| 持续触发区域 | Area2D + 信号 | 记忆内部物体,信号高效 |
| 一次性拾取物(金币) | Area2D + 进入时 queue_free() | 简单,自动清理 |
| 视线检查 | RayCast2D | 高效,内置 |
| 点击选择单位 | PhysicsPointQueryParameters2D | 单次查询,无需永久节点 |
| 范围法术 | PhysicsShapeQueryParameters2D | 一次性查询,形状灵活 |
| 即时命中武器 | PhysicsRayQueryParameters2D | 命中扫描,无射弹物理 |
| 平台游戏地面检查 | RayCast2D 或向下射线投射 | 精确的边缘检测 |
# ❌ 不良做法:在 _ready() 中射线投射不起作用(物理未初始化)
func _ready() -> void:
if $RayCast2D.is_colliding(): # 总是 false!
print("击中了某物")
# ✅ 良好做法:等待物理帧
func _ready() -> void:
await get_tree().physics_frame
if $RayCast2D.is_colliding():
print("击中了某物")
# 问题:CharacterBody2D 默认的 collision_layer = 0
# 解决方案:显式设置图层
# character.gd
func _ready() -> void:
collision_layer = 0b0001 # 图层 1: 玩家
# 射线投射会击中碰撞形状的前面和后面
# 要进行单向射线投射(仅前面),请使用 Area2D 监控
# ✅ 良好做法:不需要时禁用射线投射
func _ready() -> void:
$OptionalRaycast.enabled = false
func check_vision() -> void:
$OptionalRaycast.enabled = true
$OptionalRaycast.force_raycast_update()
var sees_player := $OptionalRaycast.is_colliding()
$OptionalRaycast.enabled = false
return sees_player
# ❌ 不良做法:为很少使用的检查保持射线投射始终开启
# 对于每秒一次的视觉检查,保持 RayCast2D.enabled = true
如果你正在处理成千上万的射弹或物理物体,SceneTree 节点的开销将成为 CPU 的瓶颈。通过使用 PhysicsServer2D 和 RenderingServer 在 C++ 或 GDScript 中直接创建、移动和绘制物体,完全绕过 SceneTree。
如果你的游戏使用低物理刻度率以节省 CPU 周期(导致可见抖动),请在项目设置中启用物理插值。这可以保持较低的物理刻度率,但在渲染帧之间平滑地插值视觉变换。
extends RigidBody2D
var thrust := Vector2(0, -250)
var torque := 20000.0
# 根据 RigidBody2D 文档,我们必须使用 _integrate_forces
# 来安全地修改物理状态,而不会与物理服务器冲突。
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
if Input.is_action_pressed("ui_up"):
# 应用力时考虑当前旋转
state.apply_force(thrust.rotated(rotation))
else:
state.apply_force(Vector2.ZERO)
var rotation_dir := Input.get_axis("ui_left", "ui_right")
state.apply_torque(rotation_dir * torque)
每周安装数
85
仓库
GitHub 星标数
59
首次出现
2026年2月10日
安全审计
安装于
gemini-cli84
codex84
opencode84
amp82
github-copilot82
kimi-cli82
Expert guidance for collision detection, triggers, and raycasting in Godot 2D.
CollisionShape2D nodes — Use the shape handles in the editor, NOT the Node2D scale property. Scaling causes unpredictable physics behavior and incorrect collision normals [12].collision_layer with collision_mask — Layer = "What AM I?", Mask = "What do I DETECT?". Setting both to the same value is usually wrong [13].move_and_slide() — move_and_slide() automatically includes timestep. Only multiply gravity/acceleration by delta [14].force_raycast_update() for manual mid-frame raycasts — Raycasts update once per physics frame. If you change target_position, you MUST force an update [15].get_overlapping_bodies() every frame — It is expensive. Cache results with body_entered/body_exited signals instead [16].RigidBody2D state directly in _process — Use _integrate_forces() for safe, synchronized access to PhysicsDirectBodyState2D [17, 411].PhysicsBody2D nodes in _process() — Use _physics_process(). Moving bodies outside the physics step causes stutter and unreliable collision detection.RigidBody2D for 1000+ simple entities — Use PhysicsServer2D to bypass node overhead for massive performance gains (Swarms/Bullets) [18, 397].Area2D for high-frequency blocking (Bullets) — Area signals can be delayed. Use move_and_collide() or ShapeCast2D for frame-perfect results [19].set_deferred for immediate physics transform logic — It happens at the end of the frame. Use force_raycast_update() or PhysicsServer2D instead.PhysicsServer2D RIDs manually — They are not garbage collected and will leak memory permanently.MANDATORY : Read the script matching your use case before implementation.
Programmatic layer/mask management with named layer constants and debug visualization.
Frame-based caching for PhysicsDirectSpaceState2D queries - eliminates redundant expensive queries.
Custom physics integration patterns for CharacterBody2D. Covers non-standard gravity, forces, and manual stepping. Use for non-standard physics behavior.
PhysicsDirectSpaceState2D query patterns for raycasting, point queries, and shape queries. Use for line-of-sight, ground detection, or area scanning.
Low-level PhysicsServer2D usage for thousands of moving objects. Bypasses node overhead for massive performance gains in bullet hells or swarms.
Manual physics sub-stepping for high-velocity projectiles. Ensures frame-perfect collision for objects moving faster than the physics tick.
Thread-safe RigidBody2D modification using _integrate_forces. Ideal for teleporting bodies or applying custom impulses without jitter.
Lighweight environment sensing using PhysicsDirectSpaceState2D. Performs ray queries without the overhead of RayCast2D nodes.
Clean architectural pattern for managing complex collision layers/masks using bitwise Enums and helpers.
Optimized multicasting vision system for AI. Reuses a single RayCast2D to check multiple angles in one physics frame.
Robust AOE detection using ShapeCast2D. Provides instant collision information without the signal-lag of Area2D.
Logic for localized gravity zones (Water, Space, Wind) and manual character-weight simulation.
Expert pattern for preventing signal spam when multi-shape bodies enter triggers.
Standard configuration and runtime adjustments to ensure smooth character movement on high-refresh-rate monitors.
Direct PhysicsServer2D RID management for peak performance in massive physics simulations.
Expert bounce and friction logic implementation for precision-critical movement.
Advanced CCD management for preventing bullet tunneling at extremely high velocities.
Optimized batch movement for multiple static/animatable bodies using riders-aware logic.
# collision_layer (32 bits): What broadcast channels am I transmitting on?
# collision_mask (32 bits): What broadcast channels am I listening to?
# Example: Player vs Enemy
# Player:
# layer = 0b0001 (Channel 1: "I am a player")
# mask = 0b0110 (Channels 2+3: "I listen for enemies and walls")
# Enemy:
# layer = 0b0010 (Channel 2: "I am an enemy")
# mask = 0b0101 (Channels 1+3: "I listen for players and walls")
# ✅ GOOD: Use helper functions for clarity
func setup_player_collision() -> void:
# I am layer 1
set_collision_layer_value(1, true)
# I detect layers 2 (enemies) and 3 (world)
set_collision_mask_value(2, true)
set_collision_mask_value(3, true)
# ✅ GOOD: Bit shift for programmatic layer math
func enable_layers(base_layer: int, count: int) -> void:
var mask := 0
for i in range(count):
mask |= (1 << (base_layer + i - 1))
collision_mask = mask
# ❌ BAD: Hardcoded bitmasks without documentation
collision_mask = 0b110110 # What does this mean?!
# Pattern: Projectile that hits enemies but ignores other projectiles
# projectile.gd
extends Area2D
func _ready() -> void:
set_collision_layer_value(4, true) # Layer 4: "Projectiles"
set_collision_mask_value(2, true) # Mask Layer 2: "Enemies"
# Result: Projectiles don't collide with each other
# Pattern: One-way platform (player can jump through from below)
# platform.gd
extends StaticBody2D
@export var one_way := true
func _ready() -> void:
set_collision_layer_value(3, true) # Layer 3: "World"
if one_way:
# Use Area2D + collision exemption instead
# (Standard one-way platforms use different technique)
pass
# ❌ BAD: body_entered fires MULTIPLE times if Area2D has multiple shapes
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
print("Entered!") # Fires 3x if Area has 3 CollisionShapes!
# ✅ GOOD: Track unique bodies with Set
extends Area2D
var _active_bodies := {} # Use dict as Set
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body not in _active_bodies:
_active_bodies[body] = true
print("First entrance!") # Fires once
func _on_body_exited(body: Node2D) -> void:
_active_bodies.erase(body)
# lava_zone.gd
extends Area2D
@export var damage_per_tick := 5
@export var tick_rate := 0.5 # Damage every 0.5s
var _damage_timers := {} # body -> time_until_next_tick
func _ready() -> void:
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _on_body_entered(body: Node2D) -> void:
if body.has_method("take_damage"):
_damage_timers[body] = 0.0 # Immediate first tick
func _on_body_exited(body: Node2D) -> void:
_damage_timers.erase(body)
func _process(delta: float) -> void:
for body in _damage_timers.keys():
_damage_timers[body] -= delta
if _damage_timers[body] <= 0.0:
body.take_damage(damage_per_tick)
_damage_timers[body] = tick_rate
# enemy_vision.gd - Enemy looks toward player
extends CharacterBody2D
@onready var vision_ray: RayCast2D = $VisionRay
func can_see_target(target: Node2D) -> bool:
var direction := global_position.direction_to(target.global_position)
vision_ray.target_position = direction * 300 # 300px range
vision_ray.force_raycast_update() # CRITICAL: Update mid-frame
if vision_ray.is_colliding():
return vision_ray.get_collider() == target
return false
# platformer_controller.gd
extends CharacterBody2D
@onready var floor_front: RayCast2D = $FloorCheckFront
@onready var floor_back: RayCast2D = $FloorCheckBack
func at_ledge() -> bool:
return floor_front.is_colliding() and not floor_back.is_colliding()
func _physics_process(delta: float) -> void:
if at_ledge() and is_on_floor():
# Enemy AI: Turn around at ledges
velocity.x *= -1
# Ignore specific bodies (e.g., self)
func _ready() -> void:
$RayCast2D.add_exception(self)
$RayCast2D.add_exception($Weapon) # Ignore attached weapon collider
# Reset exclusions
$RayCast2D.clear_exceptions()
# Check if mouse click hits any physics body
func get_body_at_mouse() -> Node2D:
var mouse_pos := get_global_mouse_position()
var space := get_world_2d().direct_space_state
var query := PhysicsPointQueryParameters2D.new()
query.position = mouse_pos
query.collide_with_areas = false
query.collision_mask = 0b11111111 # All layers
var results := space.intersect_point(query, 1) # Max 1 result
if results.is_empty():
return null
return results[0].collider
# AOE damage in circle around player
func damage_nearby_enemies(center: Vector2, radius: float, damage: int) -> void:
var space := get_world_2d().direct_space_state
var query := PhysicsShapeQueryParameters2D.new()
var circle := CircleShape2D.new()
circle.radius = radius
query.shape = circle
query.transform = Transform2D(0.0, center)
query.collision_mask = 0b0010 # Layer 2: Enemies
var hits := space.intersect_shape(query)
for hit in hits:
var enemy: Node2D = hit.collider
if enemy.has_method("take_damage"):
enemy.take_damage(damage)
# Hitscan weapon (no projectile)
func fire_hitscan_weapon(from: Vector2, direction: Vector2, max_range: float) -> void:
var space := get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(from, from + direction * max_range)
query.exclude = [self]
query.collision_mask = 0b0010 # Enemies
var result := space.intersect_ray(query)
if result:
var hit_enemy: Node2D = result.collider
var hit_point: Vector2 = result.position
spawn_hit_effect(hit_point)
if hit_enemy.has_method("take_damage"):
hit_enemy.take_damage(25)
| Use Case | Method | Why |
|---|---|---|
| Continuous trigger zone | Area2D + signals | Memory of what's inside, signals are efficient |
| One-time pickup (coin) | Area2D + queue_free() on enter | Simple, automatic cleanup |
| Line-of-sight check | RayCast2D | Efficient, built-in |
| Click-to-select units | PhysicsPointQueryParameters2D | Single query, no permanent node |
| AOE spell | PhysicsShapeQueryParameters2D | One-shot query, flexible shape |
| Instant-hit weapon | PhysicsRayQueryParameters2D | Hitscan, no projectile physics |
| Platformer ground check | RayCast2D or raycast down | Precise ledge detection |
# ❌ BAD: Raycasts don't work in _ready() (physics not initialized)
func _ready() -> void:
if $RayCast2D.is_colliding(): # Always false!
print("Hit something")
# ✅ GOOD: Wait for physics frame
func _ready() -> void:
await get_tree().physics_frame
if $RayCast2D.is_colliding():
print("Hit something")
# Problem: CharacterBody2D has collision_layer = 0 by default
# Solution: Explicitly set layer
# character.gd
func _ready() -> void:
collision_layer = 0b0001 # Layer 1: Player
# Raycasts hit both front and back of collision shapes
# To raycast one-way (front only), use Area2D monitoring
# ✅ GOOD: Disable raycasts when not needed
func _ready() -> void:
$OptionalRaycast.enabled = false
func check_vision() -> void:
$OptionalRaycast.enabled = true
$OptionalRaycast.force_raycast_update()
var sees_player := $OptionalRaycast.is_colliding()
$OptionalRaycast.enabled = false
return sees_player
# ❌ BAD: Always-on raycasts for rarely-used checks
# Leave RayCast2D.enabled = true for vision checks once per second
If you are dealing with tens of thousands of projectiles or physics objects, the SceneTree node overhead will bottleneck the CPU. Bypass the SceneTree entirely by using PhysicsServer2D and RenderingServer to create, move, and draw bodies directly in C++ or GDScript.
If your game uses a low physics tick rate to save CPU cycles (causing visible jitter), enable Physics Interpolation in the Project Settings. This keeps the physics tick rate low but interpolates visual transforms smoothly over rendered frames.
extends RigidBody2D
var thrust := Vector2(0, -250)
var torque := 20000.0
# According to the RigidBody2D documentation, we must use _integrate_forces
# to safely modify physical state without fighting the physics server.
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
if Input.is_action_pressed("ui_up"):
# Apply force taking current rotation into account
state.apply_force(thrust.rotated(rotation))
else:
state.apply_force(Vector2.ZERO)
var rotation_dir := Input.get_axis("ui_left", "ui_right")
state.apply_torque(rotation_dir * torque)
Weekly Installs
85
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli84
codex84
opencode84
amp82
github-copilot82
kimi-cli82
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
123,700 周安装