mirror of
https://github.com/Ukendio/jecs.git
synced 2026-03-18 00:44:32 +00:00
Input example (#305)
This commit is contained in:
parent
fa6f5fc52a
commit
8ba09057be
2 changed files with 304 additions and 0 deletions
35
modules/Input/examples/example.luau
Normal file
35
modules/Input/examples/example.luau
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Input = require("@modules/Input/module")
|
||||
|
||||
local function cameraSystem()
|
||||
local look = Input.value2d("look")
|
||||
|
||||
-- rotate camera with look
|
||||
end
|
||||
|
||||
local function characterMovement()
|
||||
local move = Input.clamped2d("move")
|
||||
|
||||
-- humanoid:Move(move)
|
||||
|
||||
if Input.justPressed("jump") then
|
||||
-- humanoid.Jump = true
|
||||
end
|
||||
end
|
||||
|
||||
RunService.RenderStepped:Connect(function(deltaTime)
|
||||
Input.update(deltaTime)
|
||||
|
||||
Input.runPhase("RenderStepped", function()
|
||||
cameraSystem()
|
||||
end)
|
||||
end)
|
||||
|
||||
RunService.PreSimulation:Connect(function(deltaTime)
|
||||
Input.update(deltaTime)
|
||||
|
||||
Input.runPhase("PreSimulation", function()
|
||||
characterMovement()
|
||||
end)
|
||||
end)
|
||||
269
modules/Input/module.luau
Normal file
269
modules/Input/module.luau
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
local UserInputService = game:GetService("UserInputService")
|
||||
local UserGameSettings = UserSettings():GetService("UserGameSettings")
|
||||
|
||||
-- Phase 1: Collect raw inputs from Roblox APIs. Here we are using UserInputService but you could use the other APIs.
|
||||
local rawInput = {
|
||||
space = false,
|
||||
w = false,
|
||||
a = false,
|
||||
s = false,
|
||||
d = false,
|
||||
buttonA = false,
|
||||
mouseDelta = Vector2.zero,
|
||||
leftThumbstickDelta = Vector2.zero,
|
||||
rightThumbstickDelta = Vector2.zero,
|
||||
}
|
||||
|
||||
UserInputService.InputBegan:Connect(function(input, sink)
|
||||
if sink then
|
||||
return
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.W then
|
||||
rawInput.w = true
|
||||
elseif input.KeyCode == Enum.KeyCode.A then
|
||||
rawInput.a = true
|
||||
elseif input.KeyCode == Enum.KeyCode.S then
|
||||
rawInput.s = true
|
||||
elseif input.KeyCode == Enum.KeyCode.D then
|
||||
rawInput.d = true
|
||||
elseif input.KeyCode == Enum.KeyCode.Space then
|
||||
rawInput.space = true
|
||||
elseif input.KeyCode == Enum.KeyCode.ButtonA then
|
||||
rawInput.buttonA = true
|
||||
end
|
||||
end)
|
||||
|
||||
UserInputService.InputChanged:Connect(function(input, sink)
|
||||
if input.UserInputType == Enum.UserInputType.MouseMovement then
|
||||
rawInput.mouseDelta = Vector2.new(input.Delta.X, -input.Delta.Y)
|
||||
elseif input.KeyCode == Enum.KeyCode.Thumbstick1 then
|
||||
rawInput.leftThumbstickDelta = Vector2.new(input.Position.X, input.Position.Y)
|
||||
elseif input.KeyCode == Enum.KeyCode.Thumbstick2 then
|
||||
rawInput.rightThumbstickDelta = Vector2.new(input.Position.X, input.Position.Y)
|
||||
end
|
||||
end)
|
||||
|
||||
UserInputService.InputEnded:Connect(function(input, sink)
|
||||
if input.KeyCode == Enum.KeyCode.W then
|
||||
rawInput.w = false
|
||||
elseif input.KeyCode == Enum.KeyCode.A then
|
||||
rawInput.a = false
|
||||
elseif input.KeyCode == Enum.KeyCode.S then
|
||||
rawInput.s = false
|
||||
elseif input.KeyCode == Enum.KeyCode.D then
|
||||
rawInput.d = false
|
||||
elseif input.KeyCode == Enum.KeyCode.Space then
|
||||
rawInput.space = false
|
||||
elseif input.KeyCode == Enum.KeyCode.ButtonA then
|
||||
rawInput.buttonA = false
|
||||
end
|
||||
end)
|
||||
|
||||
-- Phase 2: Derive action state from raw inputs.
|
||||
|
||||
local SENSITIVITY_MOUSE = Vector2.new(1, 0.77) * math.rad(0.5)
|
||||
local SENSITIVITY_GAMEPAD = Vector2.new(1, 0.77) * math.rad(4) * 60
|
||||
|
||||
local function virtualVector2(up: boolean, down: boolean, left: boolean, right: boolean): Vector2
|
||||
local x = 0
|
||||
local y = 0
|
||||
if up then
|
||||
y += 1
|
||||
end
|
||||
if down then
|
||||
y -= 1
|
||||
end
|
||||
if left then
|
||||
x -= 1
|
||||
end
|
||||
if right then
|
||||
x += 1
|
||||
end
|
||||
|
||||
return Vector2.new(x, y)
|
||||
end
|
||||
|
||||
local function scaledDeadZone(value: number, lowerThreshold: number): number
|
||||
local lowerBound = math.max(math.abs(value) - lowerThreshold, 0)
|
||||
local scaledValue = lowerBound / (1 - lowerThreshold)
|
||||
|
||||
return math.min(scaledValue, 1) * math.sign(value)
|
||||
end
|
||||
|
||||
local function radialDeadZone(value: Vector2, threshold: number): Vector2
|
||||
local magnitude = value.Magnitude
|
||||
if magnitude == 0 then
|
||||
return Vector2.zero
|
||||
else
|
||||
return value.Unit * scaledDeadZone(magnitude, threshold)
|
||||
end
|
||||
end
|
||||
|
||||
-- Convert raw inputs into action state. This function will apply modifiers like dead zones or sensitivity multipliers.
|
||||
local function deriveActionState(deltaTime: number)
|
||||
local keyboardMove = virtualVector2(rawInput.w, rawInput.s, rawInput.a, rawInput.d)
|
||||
local gamepadMove = radialDeadZone(rawInput.leftThumbstickDelta, 0.2)
|
||||
|
||||
local mouseLook = rawInput.mouseDelta * SENSITIVITY_MOUSE
|
||||
local gamepadLook = radialDeadZone(rawInput.rightThumbstickDelta, 0.2)
|
||||
* UserGameSettings.GamepadCameraSensitivity
|
||||
* SENSITIVITY_GAMEPAD
|
||||
* deltaTime
|
||||
|
||||
return {
|
||||
boolean = {
|
||||
jump = rawInput.space or rawInput.buttonA,
|
||||
},
|
||||
value2d = {
|
||||
move = keyboardMove + gamepadMove,
|
||||
look = mouseLook + gamepadLook,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- UserGameSettings.GamepadCameraSensitivity is only updated if this is called.
|
||||
UserGameSettings:SetGamepadCameraSensitivityVisible()
|
||||
|
||||
-- 3. The API
|
||||
local ACTIONS_BOOLEAN = {
|
||||
jump = true,
|
||||
}
|
||||
|
||||
local ACTIONS_2D = {
|
||||
move = true,
|
||||
look = true,
|
||||
}
|
||||
|
||||
local DEFAULT_PHASE_STATE = {
|
||||
boolean = {},
|
||||
justPressedCounts = {} :: { [string]: number },
|
||||
justReleasedCounts = {} :: { [string]: number },
|
||||
value2d = {},
|
||||
}
|
||||
for action in ACTIONS_BOOLEAN :: any do
|
||||
DEFAULT_PHASE_STATE.boolean[action] = false
|
||||
end
|
||||
for action in ACTIONS_2D :: any do
|
||||
DEFAULT_PHASE_STATE.value2d[action] = Vector2.zero
|
||||
end
|
||||
|
||||
local function copyDeep<T>(value: T): T
|
||||
if typeof(value) == "table" then
|
||||
local clone = table.clone(value) :: any
|
||||
|
||||
for key, value in clone do
|
||||
clone[key] = copyDeep(value)
|
||||
end
|
||||
|
||||
return clone
|
||||
else
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
local lastInputState = deriveActionState(0)
|
||||
local currentPhase = DEFAULT_PHASE_STATE
|
||||
local phases = {}
|
||||
|
||||
local Input = {}
|
||||
|
||||
function Input.justPressed(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
|
||||
return currentPhase.justPressedCounts[action] ~= nil
|
||||
end
|
||||
|
||||
function Input.justReleased(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
|
||||
return currentPhase.justReleasedCounts[action] ~= nil
|
||||
end
|
||||
|
||||
function Input.pressed(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
|
||||
return currentPhase.boolean[action]
|
||||
end
|
||||
|
||||
function Input.released(action: keyof<typeof(ACTIONS_BOOLEAN)>): boolean
|
||||
return not currentPhase.boolean[action]
|
||||
end
|
||||
|
||||
function Input.value2d(action: keyof<typeof(ACTIONS_2D)>): Vector2
|
||||
return currentPhase.value2d[action]
|
||||
end
|
||||
|
||||
function Input.unit2d(action: keyof<typeof(ACTIONS_2D)>): Vector2
|
||||
local value = currentPhase.value2d[action]
|
||||
if value.Magnitude > 0 then
|
||||
return value.Unit
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
function Input.clamped2d(action: keyof<typeof(ACTIONS_2D)>): Vector2
|
||||
local value = currentPhase.value2d[action]
|
||||
if value.Magnitude > 1 then
|
||||
return value.Unit
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
function Input.runPhase(name: string, callback: () -> ())
|
||||
if not phases[name] then
|
||||
phases[name] = copyDeep(DEFAULT_PHASE_STATE)
|
||||
end
|
||||
|
||||
currentPhase = phases[name]
|
||||
|
||||
callback()
|
||||
|
||||
table.clear(currentPhase.justPressedCounts)
|
||||
table.clear(currentPhase.justReleasedCounts)
|
||||
|
||||
for action in currentPhase.boolean do
|
||||
currentPhase.boolean[action] = false
|
||||
end
|
||||
|
||||
for action in currentPhase.value2d do
|
||||
currentPhase.value2d[action] = Vector2.zero
|
||||
end
|
||||
|
||||
currentPhase = DEFAULT_PHASE_STATE
|
||||
end
|
||||
|
||||
function Input.update(deltaTime: number)
|
||||
local inputState = deriveActionState(deltaTime)
|
||||
|
||||
local presses = {}
|
||||
local releases = {}
|
||||
for action, value in inputState.boolean do
|
||||
if value and not lastInputState.boolean[action] then
|
||||
table.insert(presses, action)
|
||||
elseif not value and lastInputState.boolean[action] then
|
||||
table.insert(releases, action)
|
||||
end
|
||||
end
|
||||
|
||||
for _, phase in phases do
|
||||
for _, action in presses do
|
||||
phase.justPressedCounts[action] = (phase.justPressedCounts[action] or 0) + 1
|
||||
end
|
||||
|
||||
for _, action in releases do
|
||||
phase.justReleasedCounts[action] = (phase.justReleasedCounts[action] or 0) + 1
|
||||
end
|
||||
|
||||
for action, value in inputState.boolean do
|
||||
phase.boolean[action] = phase.boolean[action] or value
|
||||
end
|
||||
|
||||
for action, value in inputState.value2d do
|
||||
phase.value2d[action] += value
|
||||
end
|
||||
end
|
||||
|
||||
lastInputState = inputState
|
||||
|
||||
-- Reset the mouse delta.
|
||||
rawInput.mouseDelta = Vector2.zero
|
||||
end
|
||||
|
||||
return Input
|
||||
Loading…
Reference in a new issue