@gripforgeai/mcp 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -6
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +73 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +127 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +80 -14
- package/dist/server-tools.js +3 -3
- package/dist/server.js +164 -22
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +7 -2
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export function godotJoystickFiles(c) {
|
|
2
|
+
return [{ path: 'gripforge/joystick/virtual_stick.gd', content: `extends Control
|
|
3
|
+
## GripForge input.joystick: persistent analog state, one pointer per stick.
|
|
4
|
+
signal axis_changed(value: Vector2)
|
|
5
|
+
@export_range(0.0, 0.5) var deadzone: float = ${c.deadzone}
|
|
6
|
+
@export var accent: Color = Color("${c.accent}")
|
|
7
|
+
var axis := Vector2.ZERO
|
|
8
|
+
var visual := Vector2.ZERO
|
|
9
|
+
var pointer := -1
|
|
10
|
+
|
|
11
|
+
func _ready() -> void:
|
|
12
|
+
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
13
|
+
resized.connect(reset)
|
|
14
|
+
|
|
15
|
+
static func radial(value: Vector2, zone: float) -> Vector2:
|
|
16
|
+
var magnitude := value.length()
|
|
17
|
+
if magnitude <= zone: return Vector2.ZERO
|
|
18
|
+
return value / magnitude * minf(1.0, (magnitude - zone) / (1.0 - zone))
|
|
19
|
+
|
|
20
|
+
func update_pointer(screen_position: Vector2) -> void:
|
|
21
|
+
var local := get_global_transform_with_canvas().affine_inverse() * screen_position
|
|
22
|
+
var travel := minf(size.x, size.y) * 0.325
|
|
23
|
+
if travel <= 0.0: return
|
|
24
|
+
var raw := (local - size * 0.5) / travel
|
|
25
|
+
visual = raw.limit_length()
|
|
26
|
+
axis = radial(Vector2(raw.x, -raw.y), deadzone)
|
|
27
|
+
axis_changed.emit(axis)
|
|
28
|
+
queue_redraw()
|
|
29
|
+
|
|
30
|
+
func _input(event: InputEvent) -> void:
|
|
31
|
+
if not is_visible_in_tree():
|
|
32
|
+
reset()
|
|
33
|
+
return
|
|
34
|
+
if event is InputEventScreenTouch:
|
|
35
|
+
if event.canceled and pointer == event.index:
|
|
36
|
+
reset()
|
|
37
|
+
get_viewport().set_input_as_handled()
|
|
38
|
+
elif event.pressed and not event.canceled and pointer == -1 and get_global_rect().has_point(event.position):
|
|
39
|
+
pointer = event.index
|
|
40
|
+
update_pointer(event.position)
|
|
41
|
+
get_viewport().set_input_as_handled()
|
|
42
|
+
elif not event.pressed and pointer == event.index:
|
|
43
|
+
reset()
|
|
44
|
+
get_viewport().set_input_as_handled()
|
|
45
|
+
elif event is InputEventScreenDrag and pointer == event.index:
|
|
46
|
+
update_pointer(event.position)
|
|
47
|
+
get_viewport().set_input_as_handled()
|
|
48
|
+
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
49
|
+
if event.pressed and pointer == -1 and get_global_rect().has_point(event.position):
|
|
50
|
+
pointer = -2
|
|
51
|
+
update_pointer(event.position)
|
|
52
|
+
get_viewport().set_input_as_handled()
|
|
53
|
+
elif not event.pressed and pointer == -2:
|
|
54
|
+
reset()
|
|
55
|
+
get_viewport().set_input_as_handled()
|
|
56
|
+
elif event is InputEventMouseMotion and pointer == -2:
|
|
57
|
+
update_pointer(event.position)
|
|
58
|
+
get_viewport().set_input_as_handled()
|
|
59
|
+
|
|
60
|
+
func reset() -> void:
|
|
61
|
+
pointer = -1
|
|
62
|
+
axis = Vector2.ZERO
|
|
63
|
+
visual = Vector2.ZERO
|
|
64
|
+
axis_changed.emit(axis)
|
|
65
|
+
queue_redraw()
|
|
66
|
+
|
|
67
|
+
func _notification(what: int) -> void:
|
|
68
|
+
if what == NOTIFICATION_APPLICATION_FOCUS_OUT or what == NOTIFICATION_EXIT_TREE:
|
|
69
|
+
reset()
|
|
70
|
+
|
|
71
|
+
func _draw() -> void:
|
|
72
|
+
var radius := minf(size.x, size.y) * 0.5
|
|
73
|
+
draw_circle(size * 0.5, radius, Color(0.06, 0.08, 0.11, 0.72))
|
|
74
|
+
draw_arc(size * 0.5, maxf(0, radius - 1), 0, TAU, 64, Color(1, 1, 1, 0.22), 2, true)
|
|
75
|
+
draw_circle(size * 0.5 + visual * radius * 0.65, radius * 0.28, accent)
|
|
76
|
+
` }, { path: 'gripforge/joystick/joystick_rig.gd', content: `extends CanvasLayer
|
|
77
|
+
## Add this script to a CanvasLayer. No InputMap mutation or autoload required.
|
|
78
|
+
const Stick = preload("res://gripforge/joystick/virtual_stick.gd")
|
|
79
|
+
@export var dual: bool = ${c.layout === 'dual' ? 'true' : 'false'}
|
|
80
|
+
@export_range(32, 160) var radius: float = ${c.radius}
|
|
81
|
+
@export_range(0, 0.5) var deadzone: float = ${c.deadzone}
|
|
82
|
+
var left: Control
|
|
83
|
+
var right: Control
|
|
84
|
+
|
|
85
|
+
func _ready() -> void:
|
|
86
|
+
layer = 20
|
|
87
|
+
left = Stick.new()
|
|
88
|
+
left.deadzone = deadzone
|
|
89
|
+
add_child(left)
|
|
90
|
+
if dual:
|
|
91
|
+
right = Stick.new()
|
|
92
|
+
right.deadzone = deadzone
|
|
93
|
+
add_child(right)
|
|
94
|
+
get_viewport().size_changed.connect(_layout)
|
|
95
|
+
_layout()
|
|
96
|
+
|
|
97
|
+
func _layout() -> void:
|
|
98
|
+
var view := get_viewport().get_visible_rect().size
|
|
99
|
+
var r := minf(radius, maxf(24, (view.x - 72) / (4 if dual else 2)))
|
|
100
|
+
left.size = Vector2.ONE * r * 2
|
|
101
|
+
left.position = Vector2(24, maxf(0, view.y - 24 - r * 2))
|
|
102
|
+
left.reset()
|
|
103
|
+
if right:
|
|
104
|
+
right.size = left.size
|
|
105
|
+
right.position = Vector2(view.x - 24 - r * 2, left.position.y)
|
|
106
|
+
right.reset()
|
|
107
|
+
|
|
108
|
+
func movement() -> Vector2:
|
|
109
|
+
return left.axis if is_instance_valid(left) else Vector2.ZERO
|
|
110
|
+
|
|
111
|
+
func look() -> Vector2:
|
|
112
|
+
return right.axis if is_instance_valid(right) else Vector2.ZERO
|
|
113
|
+
` }, { path: 'gripforge/joystick/joystick.tscn', content: `[gd_scene load_steps=2 format=3]
|
|
114
|
+
[ext_resource type="Script" path="res://gripforge/joystick/joystick_rig.gd" id="1"]
|
|
115
|
+
[node name="GripForgeJoystick" type="CanvasLayer"]
|
|
116
|
+
script = ExtResource("1")
|
|
117
|
+
` }, { path: 'gripforge/joystick/character_example.gd', content: `extends CharacterBody3D
|
|
118
|
+
## Optional example; merge input reads into your own motor instead of running two motors.
|
|
119
|
+
@export var joystick: CanvasLayer
|
|
120
|
+
@export var camera: Node3D
|
|
121
|
+
@export var speed: float = 5.0
|
|
122
|
+
@export var gravity: float = 9.8
|
|
123
|
+
func _physics_process(delta: float) -> void:
|
|
124
|
+
var axes: Vector2 = joystick.movement() if joystick else Vector2.ZERO
|
|
125
|
+
var yaw: float = camera.global_rotation.y if camera else global_rotation.y
|
|
126
|
+
var direction := Basis(Vector3.UP, yaw) * Vector3(axes.x, 0, -axes.y)
|
|
127
|
+
velocity.x = direction.x * speed
|
|
128
|
+
velocity.z = direction.z * speed
|
|
129
|
+
if not is_on_floor(): velocity.y -= gravity * delta
|
|
130
|
+
else: velocity.y = 0
|
|
131
|
+
move_and_slide()
|
|
132
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Godot 4.2+
|
|
133
|
+
|
|
134
|
+
Copy gripforge/ into res://. Instance joystick.tscn in the gameplay scene.
|
|
135
|
+
Read movement() and look() every physics frame. X is right; Y is forward/up.
|
|
136
|
+
Assign the rig and camera in the optional CharacterBody3D example. A collider and
|
|
137
|
+
floor are still supplied by your game; the joystick does not replace collision.
|
|
138
|
+
|
|
139
|
+
Touch IDs are isolated; both sticks can be held simultaneously. Mouse testing is
|
|
140
|
+
built in: you do not need to enable Emulate Touch From Mouse. Keep only one such
|
|
141
|
+
emulation path active. Release, focus loss, resizing and removal reset the axes.
|
|
142
|
+
The rig does not erase keyboard/gamepad bindings. Combine its movement vector with
|
|
143
|
+
your existing input source in your motor (choose one source per frame, then clamp
|
|
144
|
+
the combined length to one). Apply look as angular velocity times delta once.
|
|
145
|
+
` }];
|
|
146
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { threeJoystickFiles } from './threejs.js';
|
|
2
|
+
import { godotJoystickFiles } from './godot.js';
|
|
3
|
+
import { unityJoystickFiles } from './unity.js';
|
|
4
|
+
import { unrealJoystickFiles } from './unreal.js';
|
|
5
|
+
export const joystickDefaults = { layout: 'dual', deadzone: .15, radius: 72, accent: '#ff681f' };
|
|
6
|
+
/** Radial, continuous, unit-disc axes. Y is up/forward on every target. */
|
|
7
|
+
export function joystickAxis(x, y, deadzone = .15) {
|
|
8
|
+
if (![x, y, deadzone].every(Number.isFinite) || deadzone < 0 || deadzone >= 1)
|
|
9
|
+
return { x: 0, y: 0 };
|
|
10
|
+
const length = Math.hypot(x, y);
|
|
11
|
+
if (length <= deadzone)
|
|
12
|
+
return { x: 0, y: 0 };
|
|
13
|
+
const scale = Math.min(1, (length - deadzone) / (1 - deadzone)) / length;
|
|
14
|
+
return { x: x * scale, y: y * scale };
|
|
15
|
+
}
|
|
16
|
+
export function createJoystickKit(target, options = {}) {
|
|
17
|
+
const config = { ...joystickDefaults, ...options };
|
|
18
|
+
if (!['threejs', 'godot', 'unity', 'unreal', 'all'].includes(target))
|
|
19
|
+
throw Error('Unknown joystick target.');
|
|
20
|
+
if (!['move', 'dual'].includes(config.layout) || !Number.isFinite(config.deadzone) || config.deadzone < 0 || config.deadzone > .5 ||
|
|
21
|
+
!Number.isFinite(config.radius) || config.radius < 32 || config.radius > 160 || !/^#[\da-f]{6}$/i.test(config.accent))
|
|
22
|
+
throw Error('Invalid joystick configuration.');
|
|
23
|
+
const targets = target === 'all' ? ['threejs', 'godot', 'unity', 'unreal'] : [target];
|
|
24
|
+
const factories = { threejs: threeJoystickFiles, godot: godotJoystickFiles, unity: unityJoystickFiles, unreal: unrealJoystickFiles };
|
|
25
|
+
return {
|
|
26
|
+
schema: 'gripforge.joystick-kit.v1', module: 'input.joystick', version: '1.0.0', credits: 0, config,
|
|
27
|
+
conventions: { axes: 'x right, y up/forward', range: 'unit disc; diagonals never exceed magnitude 1', deadzone: 'radial, rescaled continuously outside the dead zone', holding: 'value persists every frame until release/cancel/focus loss', ownership: 'one pointer per stick; movement and look can be held simultaneously', physics: 'Feeds the existing pawn/controller; examples are optional.' },
|
|
28
|
+
deliveries: targets.map(engine => ({ engine, files: factories[engine](config) })),
|
|
29
|
+
status: 'files_generated',
|
|
30
|
+
next: 'Write each delivery file at its relative path, then follow that target README. Install only one movement driver. Generation does not modify your project or claim an in-game verification.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function threeJoystickFiles(c) {
|
|
2
|
+
return [{ path: 'gripforge/joystick.mjs', content: `// GripForge input.joystick 1.0.0 — no framework or renderer dependency.
|
|
3
|
+
export const defaults = ${JSON.stringify(c)};
|
|
4
|
+
export function radialAxis(x,y,deadzone=defaults.deadzone) {
|
|
5
|
+
const m=Math.hypot(x,y);
|
|
6
|
+
if(!Number.isFinite(m)||m<=deadzone) return {x:0,y:0};
|
|
7
|
+
const k=Math.min(1,(m-deadzone)/(1-deadzone))/m;
|
|
8
|
+
return {x:x*k,y:y*k};
|
|
9
|
+
}
|
|
10
|
+
export function createJoystick({parent=document.body,...options}={}) {
|
|
11
|
+
const cfg={...defaults,...options};
|
|
12
|
+
if(!['move','dual'].includes(cfg.layout)||!Number.isFinite(cfg.deadzone)||cfg.deadzone<0||cfg.deadzone>.5||!Number.isFinite(cfg.radius)||cfg.radius<32||cfg.radius>160||!/^#[a-f0-9]{6}$/i.test(cfg.accent)) throw Error('Invalid joystick options');
|
|
13
|
+
const root=document.createElement('div');
|
|
14
|
+
root.dataset.gripforgeJoystick='';
|
|
15
|
+
Object.assign(root.style,{position:'fixed',inset:'0',pointerEvents:'none',zIndex:'30'});
|
|
16
|
+
const state={move:{x:0,y:0},look:{x:0,y:0}};
|
|
17
|
+
const sticks=[];
|
|
18
|
+
function make(name,right) {
|
|
19
|
+
const base=document.createElement('div'),thumb=document.createElement('div');
|
|
20
|
+
base.dataset.stick=name; base.setAttribute('aria-label',name==='move'?'Movement joystick':'Camera joystick');
|
|
21
|
+
base.setAttribute('role','group');
|
|
22
|
+
Object.assign(base.style,{position:'absolute',bottom:'max(24px, env(safe-area-inset-bottom))',width:'min('+cfg.radius*2+'px, calc((100vw - 72px) / 2))',aspectRatio:'1',borderRadius:'50%',background:'rgba(15,20,28,.72)',boxShadow:'inset 0 0 0 2px rgba(255,255,255,.22)',touchAction:'none',userSelect:'none',pointerEvents:'auto'});
|
|
23
|
+
base.style[right?'right':'left']='max(24px, env(safe-area-inset-'+(right?'right':'left')+'))';
|
|
24
|
+
Object.assign(thumb.style,{position:'absolute',left:'36%',top:'36%',width:'28%',height:'28%',borderRadius:'50%',background:cfg.accent,boxShadow:'0 0 16px '+cfg.accent+'55',pointerEvents:'none'});
|
|
25
|
+
base.append(thumb);root.append(base);
|
|
26
|
+
const stick={base,thumb,pointer:null}; sticks.push(stick);
|
|
27
|
+
function reset(){
|
|
28
|
+
const pointer=stick.pointer;stick.pointer=null;state[name]={x:0,y:0};thumb.style.transform='';
|
|
29
|
+
if(pointer!==null&&base.hasPointerCapture(pointer))base.releasePointerCapture(pointer);
|
|
30
|
+
}
|
|
31
|
+
stick.reset=reset;
|
|
32
|
+
function update(e){
|
|
33
|
+
const r=base.getBoundingClientRect(),travel=Math.min(r.width,r.height)*.325;
|
|
34
|
+
if(travel<=0){reset();return;}
|
|
35
|
+
const x=(e.clientX-r.left-r.width/2)/travel,y=-(e.clientY-r.top-r.height/2)/travel;
|
|
36
|
+
state[name]=radialAxis(x,y,cfg.deadzone);
|
|
37
|
+
const m=Math.max(1,Math.hypot(x,y));
|
|
38
|
+
thumb.style.transform='translate('+(x/m*travel)+'px,'+(-y/m*travel)+'px)';
|
|
39
|
+
}
|
|
40
|
+
base.addEventListener('pointerdown',e=>{
|
|
41
|
+
if(stick.pointer!==null||(e.pointerType==='mouse'&&e.button!==0))return;
|
|
42
|
+
e.preventDefault();e.stopPropagation();stick.pointer=e.pointerId;base.setPointerCapture(e.pointerId);update(e);
|
|
43
|
+
});
|
|
44
|
+
base.addEventListener('pointermove',e=>{if(stick.pointer===e.pointerId){e.preventDefault();e.stopPropagation();update(e);}});
|
|
45
|
+
for(const event of ['pointerup','pointercancel','lostpointercapture'])base.addEventListener(event,e=>{if(stick.pointer===e.pointerId){e.stopPropagation();reset();}});
|
|
46
|
+
}
|
|
47
|
+
make('move',false);if(cfg.layout==='dual')make('look',true);
|
|
48
|
+
parent.append(root);
|
|
49
|
+
const reset=()=>sticks.forEach(s=>s.reset());
|
|
50
|
+
const onVisibility=()=>{if(document.hidden)reset();};
|
|
51
|
+
window.addEventListener('blur',reset);window.addEventListener('resize',reset);document.addEventListener('visibilitychange',onVisibility);
|
|
52
|
+
return {root,read:()=>({move:{...state.move},look:{...state.look}}),reset,
|
|
53
|
+
dispose(){reset();window.removeEventListener('blur',reset);window.removeEventListener('resize',reset);document.removeEventListener('visibilitychange',onVisibility);root.remove();}};
|
|
54
|
+
}
|
|
55
|
+
// Optional GripForge KitHost bridge. Does not overwrite physical gamepad state.
|
|
56
|
+
export function bindGripForgeInput(joystick,input,source='touch-joystick') {
|
|
57
|
+
if(typeof input.setVirtualAxis!=='function')throw Error('Update GripForge input.actions for named virtual axes.');
|
|
58
|
+
return {update(){const s=joystick.read();for(const [name,value] of Object.entries({move_x:s.move.x,move_y:s.move.y,look_x:s.look.x,look_y:s.look.y}))input.setVirtualAxis(name,value,source);},
|
|
59
|
+
dispose(){input.clearVirtualInput(source);joystick.reset();}};
|
|
60
|
+
}
|
|
61
|
+
` }, { path: 'gripforge/three-joystick-example.mjs', content: `import * as THREE from 'three';
|
|
62
|
+
import { createJoystick } from './joystick.mjs';
|
|
63
|
+
export const joystick=createJoystick();
|
|
64
|
+
const forward=new THREE.Vector3(),right=new THREE.Vector3();
|
|
65
|
+
// Feed this direction to your capsule/character motor's normal physics update.
|
|
66
|
+
export function readMovement(camera) {
|
|
67
|
+
const {move,look}=joystick.read();
|
|
68
|
+
camera.getWorldDirection(forward);forward.y=0;
|
|
69
|
+
if(forward.lengthSq()<.00001)forward.set(0,0,-1);else forward.normalize();
|
|
70
|
+
right.crossVectors(forward,THREE.Object3D.DEFAULT_UP).normalize();
|
|
71
|
+
return {direction:new THREE.Vector3().addScaledVector(right,move.x).addScaledVector(forward,move.y),look};
|
|
72
|
+
}
|
|
73
|
+
// Each physics tick: motor.setDesiredDirection(readMovement(camera).direction).
|
|
74
|
+
// Camera: yaw -= look.x * radiansPerSecond * dt; pitch += look.y * radiansPerSecond * dt.
|
|
75
|
+
// On unmount: joystick.dispose(). No teleporting or replacement of collision/network code.
|
|
76
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Three.js
|
|
77
|
+
|
|
78
|
+
Copy gripforge/ into your browser project. Import createJoystick() from joystick.mjs;
|
|
79
|
+
call read() every simulation frame. The overlay works with touch and mouse, holds
|
|
80
|
+
each finger independently and resets on release, cancellation, blur and disposal.
|
|
81
|
+
Outputs are move/look vectors in a unit disc, X right and Y forward/up.
|
|
82
|
+
|
|
83
|
+
The Three.js example converts movement to camera-relative world space. Pass that
|
|
84
|
+
vector to your existing physics motor. Its look vector is an angular velocity;
|
|
85
|
+
multiply by dt once. Destroy the overlay when unloading the game.
|
|
86
|
+
|
|
87
|
+
For a GripForge KitHost use bindGripForgeInput(), call its update() before
|
|
88
|
+
host.tick(), and dispose both the binding and joystick. Register look_x/look_y
|
|
89
|
+
actions when using a camera kit. Virtual axes bypass the extra hardware noise
|
|
90
|
+
threshold because the joystick already applies its radial deadzone. Physical
|
|
91
|
+
gamepad state remains independent.
|
|
92
|
+
` }];
|
|
93
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export function unityJoystickFiles(c) {
|
|
2
|
+
const dir = 'Assets/GripForge/Joystick/';
|
|
3
|
+
return [{ path: dir + 'GripForgeVirtualStick.cs', content: `using UnityEngine;
|
|
4
|
+
using UnityEngine.EventSystems;
|
|
5
|
+
|
|
6
|
+
namespace GripForge.Inputs {
|
|
7
|
+
// One owner per stick; holding a finger does not require new drag events.
|
|
8
|
+
public sealed class GripForgeVirtualStick : MonoBehaviour, IPointerDownHandler,
|
|
9
|
+
IDragHandler, IPointerUpHandler, IEndDragHandler, IInitializePotentialDragHandler {
|
|
10
|
+
[Range(0, .5f)] public float Deadzone = ${c.deadzone}f;
|
|
11
|
+
public RectTransform Thumb;
|
|
12
|
+
public Vector2 Axis { get; private set; }
|
|
13
|
+
int? owner;
|
|
14
|
+
RectTransform Rect => (RectTransform)transform;
|
|
15
|
+
|
|
16
|
+
public static Vector2 Radial(Vector2 value, float deadzone) {
|
|
17
|
+
float m = value.magnitude;
|
|
18
|
+
if (float.IsNaN(m) || float.IsInfinity(m) || m <= deadzone) return Vector2.zero;
|
|
19
|
+
return value / m * Mathf.Min(1, (m - deadzone) / (1 - deadzone));
|
|
20
|
+
}
|
|
21
|
+
public void OnInitializePotentialDrag(PointerEventData e) { e.useDragThreshold = false; }
|
|
22
|
+
public void OnPointerDown(PointerEventData e) {
|
|
23
|
+
if (owner.HasValue || e.button != PointerEventData.InputButton.Left) return;
|
|
24
|
+
owner = e.pointerId;
|
|
25
|
+
UpdatePointer(e);
|
|
26
|
+
}
|
|
27
|
+
public void OnDrag(PointerEventData e) { if (owner == e.pointerId) UpdatePointer(e); }
|
|
28
|
+
public void OnPointerUp(PointerEventData e) { if (owner == e.pointerId) ResetStick(); }
|
|
29
|
+
public void OnEndDrag(PointerEventData e) { if (owner == e.pointerId) ResetStick(); }
|
|
30
|
+
void UpdatePointer(PointerEventData e) {
|
|
31
|
+
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(Rect, e.position, e.pressEventCamera, out Vector2 p)) return;
|
|
32
|
+
float travel = Mathf.Min(Rect.rect.width, Rect.rect.height) * .325f;
|
|
33
|
+
if (travel <= 0) { ResetStick(); return; }
|
|
34
|
+
Vector2 raw = (p - Rect.rect.center) / travel;
|
|
35
|
+
Axis = Radial(raw, Deadzone);
|
|
36
|
+
if (Thumb) Thumb.anchoredPosition = Vector2.ClampMagnitude(raw, 1) * travel;
|
|
37
|
+
}
|
|
38
|
+
public void ResetStick() { owner = null; Axis = Vector2.zero; if (Thumb) Thumb.anchoredPosition = Vector2.zero; }
|
|
39
|
+
void OnDisable() { ResetStick(); }
|
|
40
|
+
void OnApplicationFocus(bool focused) { if (!focused) ResetStick(); }
|
|
41
|
+
void OnApplicationPause(bool paused) { if (paused) ResetStick(); }
|
|
42
|
+
void OnRectTransformDimensionsChange() { ResetStick(); }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
` }, { path: dir + 'GripForgeJoystickRig.cs', content: `using UnityEngine;
|
|
46
|
+
using UnityEngine.UI;
|
|
47
|
+
using UnityEngine.EventSystems;
|
|
48
|
+
#if ENABLE_INPUT_SYSTEM
|
|
49
|
+
using UnityEngine.InputSystem.UI;
|
|
50
|
+
#endif
|
|
51
|
+
|
|
52
|
+
namespace GripForge.Inputs {
|
|
53
|
+
// Add this component to a scene GameObject. It creates its own UI only.
|
|
54
|
+
public sealed class GripForgeJoystickRig : MonoBehaviour {
|
|
55
|
+
public bool Dual = ${c.layout === 'dual' ? 'true' : 'false'};
|
|
56
|
+
[Range(32, 160)] public float Radius = ${c.radius}f;
|
|
57
|
+
[Range(0, .5f)] public float Deadzone = ${c.deadzone}f;
|
|
58
|
+
public Vector2 Move => left ? left.Axis : Vector2.zero;
|
|
59
|
+
public Vector2 Look => right ? right.Axis : Vector2.zero;
|
|
60
|
+
GripForgeVirtualStick left, right;
|
|
61
|
+
GameObject ui;
|
|
62
|
+
RectTransform safe;
|
|
63
|
+
Rect lastSafe;
|
|
64
|
+
Vector2Int lastScreen;
|
|
65
|
+
Texture2D circleTexture;
|
|
66
|
+
Sprite circle;
|
|
67
|
+
|
|
68
|
+
void Awake() {
|
|
69
|
+
circleTexture = new Texture2D(64, 64, TextureFormat.RGBA32, false);
|
|
70
|
+
circleTexture.wrapMode = TextureWrapMode.Clamp;
|
|
71
|
+
for (int y=0;y<64;y++) for (int x=0;x<64;x++) {
|
|
72
|
+
float alpha = Mathf.Clamp01(32 - Vector2.Distance(new Vector2(x+.5f,y+.5f), new Vector2(32,32)));
|
|
73
|
+
circleTexture.SetPixel(x,y,new Color(1,1,1,alpha));
|
|
74
|
+
}
|
|
75
|
+
circleTexture.Apply();
|
|
76
|
+
circle = Sprite.Create(circleTexture,new Rect(0,0,64,64),new Vector2(.5f,.5f),64);
|
|
77
|
+
ui = new GameObject("GripForge Joystick UI",typeof(RectTransform),typeof(Canvas),typeof(CanvasScaler),typeof(GraphicRaycaster));
|
|
78
|
+
ui.transform.SetParent(transform,false);
|
|
79
|
+
ui.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
|
80
|
+
ui.GetComponent<Canvas>().sortingOrder = 20;
|
|
81
|
+
var scaler = ui.GetComponent<CanvasScaler>();
|
|
82
|
+
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
83
|
+
scaler.referenceResolution = new Vector2(1280,720); scaler.matchWidthOrHeight = .5f;
|
|
84
|
+
safe = new GameObject("Safe Area",typeof(RectTransform)).GetComponent<RectTransform>();
|
|
85
|
+
safe.SetParent(ui.transform,false);
|
|
86
|
+
left = MakeStick("Move",false);
|
|
87
|
+
if (Dual) right = MakeStick("Look",true);
|
|
88
|
+
// Reuse the game's EventSystem; do not replace configured input modules.
|
|
89
|
+
if (!FindObjectOfType<EventSystem>()) {
|
|
90
|
+
var events = new GameObject("GripForge EventSystem",typeof(EventSystem));
|
|
91
|
+
events.transform.SetParent(transform,false);
|
|
92
|
+
#if ENABLE_INPUT_SYSTEM
|
|
93
|
+
var input = events.AddComponent<InputSystemUIInputModule>();
|
|
94
|
+
input.AssignDefaultActions();
|
|
95
|
+
#else
|
|
96
|
+
events.AddComponent<StandaloneInputModule>();
|
|
97
|
+
#endif
|
|
98
|
+
}
|
|
99
|
+
UpdateLayout();
|
|
100
|
+
}
|
|
101
|
+
GripForgeVirtualStick MakeStick(string name, bool rightSide) {
|
|
102
|
+
var go = new GameObject(name,typeof(RectTransform),typeof(Image),typeof(GripForgeVirtualStick));
|
|
103
|
+
var r = (RectTransform)go.transform; r.SetParent(safe,false);
|
|
104
|
+
r.anchorMin = r.anchorMax = new Vector2(rightSide ? 1 : 0,0);
|
|
105
|
+
r.pivot = new Vector2(.5f,.5f);
|
|
106
|
+
var image = go.GetComponent<Image>(); image.sprite = circle; image.color = new Color(.06f,.08f,.11f,.72f);
|
|
107
|
+
var thumb = new GameObject("Thumb",typeof(RectTransform),typeof(Image));
|
|
108
|
+
var tr = (RectTransform)thumb.transform; tr.SetParent(r,false);
|
|
109
|
+
tr.anchorMin = tr.anchorMax = tr.pivot = new Vector2(.5f,.5f);
|
|
110
|
+
var ti = thumb.GetComponent<Image>(); ti.sprite = circle; ti.raycastTarget = false;
|
|
111
|
+
ColorUtility.TryParseHtmlString("${c.accent}",out Color accent); ti.color = accent;
|
|
112
|
+
var stick = go.GetComponent<GripForgeVirtualStick>(); stick.Thumb = tr; stick.Deadzone = Deadzone;
|
|
113
|
+
return stick;
|
|
114
|
+
}
|
|
115
|
+
void Update() {
|
|
116
|
+
if (lastSafe != Screen.safeArea || lastScreen != new Vector2Int(Screen.width,Screen.height)) UpdateLayout();
|
|
117
|
+
}
|
|
118
|
+
void UpdateLayout() {
|
|
119
|
+
if (!safe || Screen.width <= 0 || Screen.height <= 0) return;
|
|
120
|
+
lastSafe = Screen.safeArea; lastScreen = new Vector2Int(Screen.width,Screen.height);
|
|
121
|
+
safe.anchorMin = new Vector2(lastSafe.xMin/Screen.width,lastSafe.yMin/Screen.height);
|
|
122
|
+
safe.anchorMax = new Vector2(lastSafe.xMax/Screen.width,lastSafe.yMax/Screen.height);
|
|
123
|
+
safe.offsetMin = safe.offsetMax = Vector2.zero;
|
|
124
|
+
Canvas.ForceUpdateCanvases();
|
|
125
|
+
float radius = Mathf.Min(Radius, Mathf.Max(24,(safe.rect.width-72)/(Dual?4:2)));
|
|
126
|
+
Layout(left,false,radius); if(right) Layout(right,true,radius);
|
|
127
|
+
}
|
|
128
|
+
void Layout(GripForgeVirtualStick stick, bool rightSide, float radius) {
|
|
129
|
+
var r = (RectTransform)stick.transform;
|
|
130
|
+
r.sizeDelta = Vector2.one * radius * 2;
|
|
131
|
+
r.anchoredPosition = new Vector2((rightSide?-1:1)*(24+radius),24+radius);
|
|
132
|
+
stick.Thumb.sizeDelta = Vector2.one * radius * .56f;
|
|
133
|
+
stick.ResetStick();
|
|
134
|
+
}
|
|
135
|
+
void OnEnable() { if(ui) ui.SetActive(true); }
|
|
136
|
+
void OnDisable() { if(ui) ui.SetActive(false); }
|
|
137
|
+
void OnDestroy() {
|
|
138
|
+
if(ui) Destroy(ui); if(circle) Destroy(circle); if(circleTexture) Destroy(circleTexture);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
` }, { path: dir + 'GripForgeJoystickMotorExample.cs', content: `using UnityEngine;
|
|
143
|
+
namespace GripForge.Inputs {
|
|
144
|
+
// Optional example. Prefer reading Joystick.Move in the motor already in your game.
|
|
145
|
+
[RequireComponent(typeof(CharacterController))]
|
|
146
|
+
public sealed class GripForgeJoystickMotorExample : MonoBehaviour {
|
|
147
|
+
public GripForgeJoystickRig Joystick;
|
|
148
|
+
public Transform View;
|
|
149
|
+
public float Speed = 5, Gravity = -20;
|
|
150
|
+
CharacterController motor;
|
|
151
|
+
float vertical;
|
|
152
|
+
void Awake() { motor = GetComponent<CharacterController>(); }
|
|
153
|
+
void Update() {
|
|
154
|
+
Vector2 axes = Joystick ? Joystick.Move : Vector2.zero;
|
|
155
|
+
float yaw = View ? View.eulerAngles.y : transform.eulerAngles.y;
|
|
156
|
+
Vector3 velocity = Quaternion.Euler(0,yaw,0) * new Vector3(axes.x,0,axes.y) * Speed;
|
|
157
|
+
if (motor.isGrounded && vertical < 0) vertical = -2;
|
|
158
|
+
vertical += Gravity * Time.deltaTime; velocity.y = vertical;
|
|
159
|
+
motor.Move(velocity * Time.deltaTime);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Unity 2022.3 / Unity 6
|
|
164
|
+
|
|
165
|
+
Copy Assets/GripForge/Joystick into your project. Install Unity UI (com.unity.ugui)
|
|
166
|
+
if your project does not include it. Add GripForgeJoystickRig to a scene GameObject
|
|
167
|
+
and press Play. It creates the overlay and reuses your EventSystem, or creates one
|
|
168
|
+
with the enabled input backend (legacy or Input System). An existing EventSystem
|
|
169
|
+
must have a working UI input module; do not add a second one.
|
|
170
|
+
|
|
171
|
+
Read rig.Move and rig.Look each frame. X is right; Y is forward/up. Feed Move into
|
|
172
|
+
your existing movement controller; multiply Look by your angular speed and deltaTime.
|
|
173
|
+
The optional MotorExample requires the built-in Physics module and uses
|
|
174
|
+
CharacterController.Move and gravity. Assign the
|
|
175
|
+
rig and camera in the Inspector; do not run it alongside another movement motor.
|
|
176
|
+
|
|
177
|
+
The two sticks have independent pointer ownership and a radial deadzone. Axes
|
|
178
|
+
persist while held, reset when released, disabled, resized or focus is lost. The
|
|
179
|
+
UI respects the safe area. The component does not rewrite gamepad/Input Actions.
|
|
180
|
+
Disable/remove the rig to remove its controls. Mouse testing is supported.
|
|
181
|
+
` }];
|
|
182
|
+
}
|