@quenty/camera 14.40.1 → 14.41.1
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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [14.41.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.41.0...@quenty/camera@14.41.1) (2026-06-05)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- Pass input ([faf1d56](https://github.com/Quenty/NevermoreEngine/commit/faf1d567726e8d640d8fd0060342fc200840a640))
|
|
11
|
+
|
|
12
|
+
# [14.41.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.40.1...@quenty/camera@14.41.0) (2026-06-03)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- ConstrainedLookCamera ([8247ba8](https://github.com/Quenty/NevermoreEngine/commit/8247ba88b84c8f209bd1f72b7a384b907f735338))
|
|
17
|
+
|
|
6
18
|
## [14.40.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.40.0...@quenty/camera@14.40.1) (2026-05-30)
|
|
7
19
|
|
|
8
20
|
**Note:** Version bump only for package @quenty/camera
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/camera",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.41.1",
|
|
4
4
|
"description": "Quenty's camera system for Roblox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "4f028984d278f4c3c77ab60ae1ce11cbaf00cb6d"
|
|
57
57
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Input handler that drives a [ConstrainedLookCamera]. Reacts to right-click
|
|
4
|
+
drag on mouse, drag on touchscreen, and right thumbstick on gamepad.
|
|
5
|
+
|
|
6
|
+
@class ConstrainedLookControls
|
|
7
|
+
]=]
|
|
8
|
+
|
|
9
|
+
local require = require(script.Parent.loader).load(script)
|
|
10
|
+
|
|
11
|
+
local ContextActionService = game:GetService("ContextActionService")
|
|
12
|
+
local RunService = game:GetService("RunService")
|
|
13
|
+
local UserInputService = game:GetService("UserInputService")
|
|
14
|
+
|
|
15
|
+
local ConstrainedLookCamera = require("ConstrainedLookCamera")
|
|
16
|
+
local GamepadRotateModel = require("GamepadRotateModel")
|
|
17
|
+
local InputObjectUtils = require("InputObjectUtils")
|
|
18
|
+
local Maid = require("Maid")
|
|
19
|
+
|
|
20
|
+
local ConstrainedLookControls = {}
|
|
21
|
+
ConstrainedLookControls.__index = ConstrainedLookControls
|
|
22
|
+
ConstrainedLookControls.ClassName = "ConstrainedLookControls"
|
|
23
|
+
|
|
24
|
+
ConstrainedLookControls.MOUSE_SENSITIVITY = Vector2.new(math.pi * 4, math.pi * 1.9)
|
|
25
|
+
ConstrainedLookControls.GAMEPAD_SENSITIVITY = 0.1
|
|
26
|
+
ConstrainedLookControls._dragBeginTypes = { Enum.UserInputType.MouseButton2, Enum.UserInputType.Touch }
|
|
27
|
+
|
|
28
|
+
export type ConstrainedLookControls = typeof(setmetatable(
|
|
29
|
+
{} :: {
|
|
30
|
+
_camera: ConstrainedLookCamera.ConstrainedLookCamera,
|
|
31
|
+
_enabled: boolean,
|
|
32
|
+
_key: string,
|
|
33
|
+
_maid: Maid.Maid?,
|
|
34
|
+
_gamepadRotateModel: GamepadRotateModel.GamepadRotateModel,
|
|
35
|
+
_lastMousePosition: Vector3?,
|
|
36
|
+
_mouseSensitivity: Vector2,
|
|
37
|
+
_gamepadSensitivity: number,
|
|
38
|
+
},
|
|
39
|
+
{} :: typeof({ __index = ConstrainedLookControls })
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
--[=[
|
|
43
|
+
Constructs a new ConstrainedLookControls bound to the given camera.
|
|
44
|
+
]=]
|
|
45
|
+
function ConstrainedLookControls.new(camera: ConstrainedLookCamera.ConstrainedLookCamera): ConstrainedLookControls
|
|
46
|
+
local self: ConstrainedLookControls = setmetatable({} :: any, ConstrainedLookControls)
|
|
47
|
+
|
|
48
|
+
self._camera = assert(camera, "Bad camera")
|
|
49
|
+
self._enabled = false
|
|
50
|
+
self._key = tostring(self) .. "ConstrainedLookControls"
|
|
51
|
+
self._mouseSensitivity = ConstrainedLookControls.MOUSE_SENSITIVITY
|
|
52
|
+
self._gamepadSensitivity = ConstrainedLookControls.GAMEPAD_SENSITIVITY
|
|
53
|
+
self._gamepadRotateModel = GamepadRotateModel.new()
|
|
54
|
+
|
|
55
|
+
return self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
--[=[
|
|
59
|
+
Sets mouse sensitivity.
|
|
60
|
+
]=]
|
|
61
|
+
function ConstrainedLookControls.SetMouseSensitivity(self: ConstrainedLookControls, sensitivity: Vector2)
|
|
62
|
+
self._mouseSensitivity = sensitivity
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
--[=[
|
|
66
|
+
Sets gamepad sensitivity.
|
|
67
|
+
]=]
|
|
68
|
+
function ConstrainedLookControls.SetGamepadSensitivity(self: ConstrainedLookControls, sensitivity: number)
|
|
69
|
+
self._gamepadSensitivity = sensitivity
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
--[=[
|
|
73
|
+
Returns whether controls are enabled.
|
|
74
|
+
]=]
|
|
75
|
+
function ConstrainedLookControls.IsEnabled(self: ConstrainedLookControls): boolean
|
|
76
|
+
return self._enabled
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
--[=[
|
|
80
|
+
Enables input. Binds drag (MouseButton2 / Touch) and gamepad Thumbstick2.
|
|
81
|
+
]=]
|
|
82
|
+
function ConstrainedLookControls.Enable(self: ConstrainedLookControls)
|
|
83
|
+
if self._enabled then
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
assert(not self._maid, "Maid already defined")
|
|
87
|
+
self._enabled = true
|
|
88
|
+
|
|
89
|
+
local maid = Maid.new()
|
|
90
|
+
self._maid = maid
|
|
91
|
+
|
|
92
|
+
maid:GiveTask(self._gamepadRotateModel.IsRotating.Changed:Connect(function()
|
|
93
|
+
if self._gamepadRotateModel.IsRotating.Value then
|
|
94
|
+
self:_handleGamepadRotateStart()
|
|
95
|
+
else
|
|
96
|
+
self:_handleGamepadRotateStop()
|
|
97
|
+
end
|
|
98
|
+
end))
|
|
99
|
+
|
|
100
|
+
ContextActionService:BindAction(self._key .. "Drag", function(_, userInputState, inputObject)
|
|
101
|
+
if userInputState == Enum.UserInputState.Begin then
|
|
102
|
+
self:_beginDrag(inputObject)
|
|
103
|
+
end
|
|
104
|
+
return Enum.ContextActionResult.Pass
|
|
105
|
+
end, false, unpack(self._dragBeginTypes))
|
|
106
|
+
|
|
107
|
+
ContextActionService:BindAction(self._key .. "Rotate", function(_, _, inputObject)
|
|
108
|
+
self._gamepadRotateModel:HandleThumbstickInput(inputObject)
|
|
109
|
+
return Enum.ContextActionResult.Pass
|
|
110
|
+
end, false, Enum.KeyCode.Thumbstick2)
|
|
111
|
+
|
|
112
|
+
maid:GiveTask(function()
|
|
113
|
+
ContextActionService:UnbindAction(self._key .. "Drag")
|
|
114
|
+
ContextActionService:UnbindAction(self._key .. "Rotate")
|
|
115
|
+
end)
|
|
116
|
+
|
|
117
|
+
maid:GiveTask(function()
|
|
118
|
+
self._camera:Release()
|
|
119
|
+
end)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
--[=[
|
|
123
|
+
Disables input and releases the camera.
|
|
124
|
+
]=]
|
|
125
|
+
function ConstrainedLookControls.Disable(self: ConstrainedLookControls)
|
|
126
|
+
if not self._enabled then
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
assert(self._maid, "Must be enabled")
|
|
130
|
+
self._enabled = false
|
|
131
|
+
|
|
132
|
+
self._maid:DoCleaning()
|
|
133
|
+
self._maid = nil
|
|
134
|
+
self._lastMousePosition = nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
function ConstrainedLookControls._beginDrag(self: ConstrainedLookControls, beginInputObject: InputObject)
|
|
138
|
+
assert(self._maid, "Must be enabled")
|
|
139
|
+
|
|
140
|
+
local maid = Maid.new()
|
|
141
|
+
self._lastMousePosition = beginInputObject.Position
|
|
142
|
+
|
|
143
|
+
local isMouse = InputObjectUtils.isMouseUserInputType(beginInputObject.UserInputType)
|
|
144
|
+
if isMouse then
|
|
145
|
+
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
|
|
146
|
+
maid:GiveTask(function()
|
|
147
|
+
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
|
|
148
|
+
end)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
maid:GiveTask(UserInputService.InputEnded:Connect(function(inputObject: InputObject)
|
|
152
|
+
if inputObject == beginInputObject then
|
|
153
|
+
self:_endDrag()
|
|
154
|
+
end
|
|
155
|
+
end))
|
|
156
|
+
|
|
157
|
+
maid:GiveTask(UserInputService.InputChanged:Connect(function(inputObject: InputObject)
|
|
158
|
+
if InputObjectUtils.isMouseUserInputType(inputObject.UserInputType) or inputObject == beginInputObject then
|
|
159
|
+
self:_handleDragMovement(inputObject)
|
|
160
|
+
end
|
|
161
|
+
end))
|
|
162
|
+
|
|
163
|
+
maid:GiveTask(function()
|
|
164
|
+
self._lastMousePosition = nil
|
|
165
|
+
self._camera:Release()
|
|
166
|
+
end)
|
|
167
|
+
|
|
168
|
+
self._maid._dragMaid = maid
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
function ConstrainedLookControls._endDrag(self: ConstrainedLookControls)
|
|
172
|
+
assert(self._maid, "Must be enabled")
|
|
173
|
+
self._maid._dragMaid = nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
function ConstrainedLookControls._handleDragMovement(self: ConstrainedLookControls, inputObject: InputObject)
|
|
177
|
+
if not self._lastMousePosition then
|
|
178
|
+
return
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
local delta = -inputObject.Delta
|
|
182
|
+
if InputObjectUtils.isMouseUserInputType(inputObject.UserInputType) then
|
|
183
|
+
delta += self._lastMousePosition - inputObject.Position
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
local xTheta = delta.X / 1920
|
|
187
|
+
local yTheta = delta.Y / 1200
|
|
188
|
+
local deltaAngle = Vector2.new(xTheta, yTheta) * self._mouseSensitivity
|
|
189
|
+
self._camera:RotateXY(deltaAngle)
|
|
190
|
+
|
|
191
|
+
self._lastMousePosition = inputObject.Position
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
function ConstrainedLookControls._handleGamepadRotateStart(self: ConstrainedLookControls)
|
|
195
|
+
assert(self._maid, "Must be enabled")
|
|
196
|
+
|
|
197
|
+
local maid = Maid.new()
|
|
198
|
+
|
|
199
|
+
maid:GiveTask(RunService.Stepped:Connect(function()
|
|
200
|
+
local deltaAngle = self._gamepadSensitivity * self._gamepadRotateModel:GetThumbstickDeltaAngle()
|
|
201
|
+
self._camera:RotateXY(deltaAngle)
|
|
202
|
+
end))
|
|
203
|
+
|
|
204
|
+
maid:GiveTask(function()
|
|
205
|
+
self._camera:Release()
|
|
206
|
+
end)
|
|
207
|
+
|
|
208
|
+
self._maid._dragMaid = maid
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
function ConstrainedLookControls._handleGamepadRotateStop(self: ConstrainedLookControls)
|
|
212
|
+
assert(self._maid, "Must be enabled")
|
|
213
|
+
self._maid._dragMaid = nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
function ConstrainedLookControls.Destroy(self: ConstrainedLookControls)
|
|
217
|
+
self:Disable()
|
|
218
|
+
self._gamepadRotateModel:Destroy()
|
|
219
|
+
setmetatable(self :: any, nil)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
return ConstrainedLookControls
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Constrains pitch and yaw within a cone.
|
|
4
|
+
|
|
5
|
+
@class ConstrainedLookCamera
|
|
6
|
+
]=]
|
|
7
|
+
|
|
8
|
+
local require = require(script.Parent.loader).load(script)
|
|
9
|
+
|
|
10
|
+
local CameraEffectUtils = require("CameraEffectUtils")
|
|
11
|
+
local CameraState = require("CameraState")
|
|
12
|
+
local Spring = require("Spring")
|
|
13
|
+
local SummedCamera = require("SummedCamera")
|
|
14
|
+
|
|
15
|
+
local ConstrainedLookCamera = {}
|
|
16
|
+
ConstrainedLookCamera.ClassName = "ConstrainedLookCamera"
|
|
17
|
+
|
|
18
|
+
export type ConstrainedLookCamera =
|
|
19
|
+
typeof(setmetatable(
|
|
20
|
+
{} :: {
|
|
21
|
+
CameraState: CameraState.CameraState,
|
|
22
|
+
CFrame: CFrame,
|
|
23
|
+
AngleYaw: number,
|
|
24
|
+
AnglePitch: number,
|
|
25
|
+
TargetAngleYaw: number,
|
|
26
|
+
TargetAnglePitch: number,
|
|
27
|
+
MaxYawOffset: number,
|
|
28
|
+
MaxPitchOffset: number,
|
|
29
|
+
Speed: number,
|
|
30
|
+
SpeedYaw: number,
|
|
31
|
+
SpeedPitch: number,
|
|
32
|
+
Damper: number,
|
|
33
|
+
SpringYaw: Spring.Spring<number>,
|
|
34
|
+
SpringPitch: Spring.Spring<number>,
|
|
35
|
+
},
|
|
36
|
+
{} :: typeof({ __index = ConstrainedLookCamera })
|
|
37
|
+
))
|
|
38
|
+
& CameraEffectUtils.CameraEffect
|
|
39
|
+
|
|
40
|
+
ConstrainedLookCamera._maxYawOffset = math.rad(20)
|
|
41
|
+
ConstrainedLookCamera._maxPitchOffset = math.rad(15)
|
|
42
|
+
|
|
43
|
+
--[=[
|
|
44
|
+
Constructs a new ConstrainedLookCamera.
|
|
45
|
+
]=]
|
|
46
|
+
function ConstrainedLookCamera.new(): ConstrainedLookCamera
|
|
47
|
+
local self: ConstrainedLookCamera = setmetatable({} :: any, ConstrainedLookCamera)
|
|
48
|
+
|
|
49
|
+
self.SpringYaw = Spring.new(0)
|
|
50
|
+
self.SpringPitch = Spring.new(0)
|
|
51
|
+
self.Speed = 15
|
|
52
|
+
self.Damper = 1
|
|
53
|
+
|
|
54
|
+
return self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
function ConstrainedLookCamera.__add(self: ConstrainedLookCamera, other: CameraEffectUtils.CameraEffect)
|
|
58
|
+
return SummedCamera.new(self, other)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
--[=[
|
|
62
|
+
Rotates the target yaw (X) and pitch (Y) by the given delta, clamped within the max offsets.
|
|
63
|
+
]=]
|
|
64
|
+
function ConstrainedLookCamera.RotateXY(self: ConstrainedLookCamera, delta: Vector2)
|
|
65
|
+
self.TargetAngleYaw += delta.X
|
|
66
|
+
self.TargetAnglePitch += delta.Y
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
--[=[
|
|
70
|
+
Releases input, sending spring targets back to the origin.
|
|
71
|
+
]=]
|
|
72
|
+
function ConstrainedLookCamera.Release(self: ConstrainedLookCamera)
|
|
73
|
+
self.TargetAngleYaw = 0
|
|
74
|
+
self.TargetAnglePitch = 0
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
--[=[
|
|
78
|
+
Snaps springs to the origin without animating.
|
|
79
|
+
]=]
|
|
80
|
+
function ConstrainedLookCamera.SnapToOrigin(self: ConstrainedLookCamera)
|
|
81
|
+
self.SpringYaw:SetTarget(0, true)
|
|
82
|
+
self.SpringPitch:SetTarget(0, true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
function ConstrainedLookCamera.__newindex(self: ConstrainedLookCamera, index, value)
|
|
86
|
+
if index == "AngleYaw" then
|
|
87
|
+
self.SpringYaw.Position = math.clamp(value, -self.MaxYawOffset, self.MaxYawOffset)
|
|
88
|
+
elseif index == "AnglePitch" then
|
|
89
|
+
self.SpringPitch.Position = math.clamp(value, -self.MaxPitchOffset, self.MaxPitchOffset)
|
|
90
|
+
elseif index == "TargetAngleYaw" then
|
|
91
|
+
self.SpringYaw.Target = math.clamp(value, -self.MaxYawOffset, self.MaxYawOffset)
|
|
92
|
+
elseif index == "TargetAnglePitch" then
|
|
93
|
+
self.SpringPitch.Target = math.clamp(value, -self.MaxPitchOffset, self.MaxPitchOffset)
|
|
94
|
+
elseif index == "MaxYawOffset" then
|
|
95
|
+
assert(value >= 0, "MaxYawOffset must be non-negative")
|
|
96
|
+
self._maxYawOffset = value
|
|
97
|
+
self.TargetAngleYaw = self.SpringYaw.Target
|
|
98
|
+
elseif index == "MaxPitchOffset" then
|
|
99
|
+
assert(value >= 0, "MaxPitchOffset must be non-negative")
|
|
100
|
+
self._maxPitchOffset = value
|
|
101
|
+
self.TargetAnglePitch = self.SpringPitch.Target
|
|
102
|
+
elseif index == "SpeedYaw" then
|
|
103
|
+
self.SpringYaw.Speed = value
|
|
104
|
+
elseif index == "SpeedPitch" then
|
|
105
|
+
self.SpringPitch.Speed = value
|
|
106
|
+
elseif index == "Speed" then
|
|
107
|
+
self.SpringYaw.Speed = value
|
|
108
|
+
self.SpringPitch.Speed = value
|
|
109
|
+
elseif index == "Damper" then
|
|
110
|
+
self.SpringYaw.Damper = value
|
|
111
|
+
self.SpringPitch.Damper = value
|
|
112
|
+
elseif ConstrainedLookCamera[index] ~= nil or index == "SpringYaw" or index == "SpringPitch" then
|
|
113
|
+
rawset(self, index, value)
|
|
114
|
+
else
|
|
115
|
+
error(`{tostring(index)} is not a valid member of ConstrainedLookCamera`)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
function ConstrainedLookCamera.__index(self: ConstrainedLookCamera, index)
|
|
120
|
+
if index == "CameraState" then
|
|
121
|
+
local state = CameraState.new()
|
|
122
|
+
state.CFrame = self.CFrame
|
|
123
|
+
return state
|
|
124
|
+
elseif index == "CFrame" then
|
|
125
|
+
return CFrame.Angles(0, self.AngleYaw, 0) * CFrame.Angles(self.AnglePitch, 0, 0)
|
|
126
|
+
elseif index == "AngleYaw" then
|
|
127
|
+
return self.SpringYaw.Position
|
|
128
|
+
elseif index == "AnglePitch" then
|
|
129
|
+
return self.SpringPitch.Position
|
|
130
|
+
elseif index == "TargetAngleYaw" then
|
|
131
|
+
return self.SpringYaw.Target
|
|
132
|
+
elseif index == "TargetAnglePitch" then
|
|
133
|
+
return self.SpringPitch.Target
|
|
134
|
+
elseif index == "MaxYawOffset" then
|
|
135
|
+
return self._maxYawOffset
|
|
136
|
+
elseif index == "MaxPitchOffset" then
|
|
137
|
+
return self._maxPitchOffset
|
|
138
|
+
else
|
|
139
|
+
return ConstrainedLookCamera[index]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return ConstrainedLookCamera
|