Files
gameserver/RCCService2018/content/internal/Common/Modules/Common/Examples/Roact-asteroids.lua
T
2026-07-06 14:01:11 -04:00

469 lines
11 KiB
Lua

--[[
Implements the start of an Asteroids clone using Roact and Rodux.
This is intended to be an advanced example that uses most edge cases of both
Roact and Rodux.
]]
local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local PlayerGui = game.Players.LocalPlayer.PlayerGui
local Common = script.Parent.Parent
local Roact = require(Common.Roact)
local ExternalEventConnection = require(Common.RoactUtilities.ExternalEventConnection)
local Rodux = require(Common.Rodux)
local RoactRodux = require(Common.RoactRodux)
local Immutable = require(Common.Immutable)
local function playerInputReducer(state, action)
state = state or {
left = 0,
right = 0,
up = 0,
pause = 0,
}
if action.type == "SetPlayerInput" then
return Immutable.Set(state, action.playerInput, action.value)
end
return state
end
local function playerReducer(state, action)
state = state or {
position = Vector2.new(0, 0),
velocity = Vector2.new(0, 0),
angle = 0,
turnRate = math.pi / 2,
throttleRate = 10,
}
if action.type == "GameStep" then
local inputs = action.inputs
local delta = action.delta
local direction = Vector2.new(math.sin(state.angle), -math.cos(state.angle))
local turnImpulse = inputs["right"] - inputs["left"]
local throttleImpulse = inputs["up"]
return Immutable.JoinDictionaries(state, {
position = state.position + (state.velocity * delta),
velocity = state.velocity + (throttleImpulse * state.throttleRate * delta * direction),
angle = state.angle + (turnImpulse * state.turnRate * delta),
})
elseif action.type == "SetPlayerRotation" then
return Immutable.Set(state, "angle", action.value)
end
return state
end
local function timeReducer(state, action)
state = state or 0
if action.type == "GameStep" then
return state + action.delta
end
return state
end
local function asteroidsReducer(state, action)
local function initAsteroids()
return {
{
position = Vector2.new(-5,-5),
velocity = Vector2.new(1,0),
size = 5,
}, {
position = Vector2.new(-5,5),
velocity = Vector2.new(1,0),
size = 3,
}, {
position = Vector2.new(5,5),
velocity = Vector2.new(0,-1),
size = 5,
}, {
position = Vector2.new(5,-5),
velocity = Vector2.new(0,1),
size = 3,
},
}
end
state = state or initAsteroids()
if action.type == "GameStep" then
local delta = action.delta
local adjustedAsteroids = {}
for index, asteroid in pairs(state) do
local posX = asteroid.position.X
local posY = asteroid.position.Y
local velX = asteroid.velocity.X
local velY = asteroid.velocity.Y
adjustedAsteroids[index] = {
position = Vector2.new(posX + velX * delta, posY + velY * delta),
velocity = Vector2.new(velX, velY),
size = asteroid.size
}
end
return adjustedAsteroids
end
return state
end
local function pausePressed(action)
return action.playerInput == "pause" and action.value == 1
end
local function pauseReducer(state, action)
state = state or false
if action.type == "SetPlayerInput" and pausePressed(action) then
return not state
end
return state
end
local function reducer(state, action)
state = state or {}
return {
playerInputs = playerInputReducer(state.playerInputs, action),
paused = pauseReducer(state.paused, action),
player = playerReducer(state.player, action),
time = timeReducer(state.time, action),
asteroids = asteroidsReducer(state.asteroids, action),
}
end
-- Defines the current camera declaratively
local Camera = Roact.Component:extend("Camera")
function Camera:init()
self.cameraRef = function(rbx)
self.cameraRbx = rbx
end
end
function Camera:render()
local focus = self.props.focus
return Roact.createElement("Camera", {
CameraType = Enum.CameraType.Scriptable,
CFrame = CFrame.new(Vector3.new(focus.x, 20, focus.y)) * CFrame.fromEulerAnglesXYZ(-math.pi / 2, 0, 0),
FieldOfView = 90,
[Roact.Ref] = self.cameraRef,
})
end
function Camera:didMount()
Workspace.CurrentCamera = self.cameraRbx
end
-- A version of the camera that's tied to the player's position.
local PlayerCamera = RoactRodux.connect(function(store)
local player = store:getState().player
return {
focus = player.position
}
end)(Camera)
-- Any asteroid in the game world.
local Asteroid = Roact.Component:extend("Asteroid")
function Asteroid:render()
local position = self.props.position
local size = self.props.size
return Roact.createElement("Part", {
Anchored = true,
Shape = Enum.PartType.Ball,
Size = Vector3.new(size, size, size),
Color = Color3.fromRGB(170, 170, 170),
CFrame = CFrame.new(Vector3.new(position.x, 0, position.y)),
TopSurface = Enum.SurfaceType.Smooth,
})
end
-- A collection of Asteroids in the world.
local Asteroids = Roact.Component:extend("Asteroids")
function Asteroids:render()
local asteroids = self.props
local asteroidComponentTable = {}
for index, asteroid in pairs(asteroids) do
asteroidComponentTable[index] = Roact.createElement(Asteroid, {
position = asteroid.position,
size = asteroid.size,
})
end
return Roact.createElement("Folder", {
Name = "Asteroids"
}, asteroidComponentTable)
end
-- Connect the Asteroids component to state.asteroids
Asteroids = RoactRodux.connect(function(store)
local state = store:getState()
local asteroids = state.asteroids
return asteroids
end)(Asteroids)
-- Any ship in the game world.
local Ship = Roact.Component:extend("Ship")
function Ship:render()
local position = self.props.position
local angle = self.props.angle
local time = self.props.time
local color = Color3.fromHSV((time % 5) / 5, 1, 1)
return Roact.createElement("Part", {
Anchored = true,
Size = Vector3.new(2, 2, 6),
Color = color,
CFrame = CFrame.new(Vector3.new(position.x, 0, position.y)) * CFrame.fromEulerAnglesXYZ(0, -angle, 0),
FrontSurface = Enum.SurfaceType.Hinge,
})
end
-- A version of the ship that's tied to the player's data.
local PlayerShip = RoactRodux.connect(function(store)
local state = store:getState()
local player = state.player
return {
position = player.position,
angle = player.angle,
time = state.time,
}
end)(Ship)
-- A static background
local GameBackground = Roact.Component:extend("GameBackground")
function GameBackground:render()
return Roact.createElement("Part", {
Anchored = true,
Size = Vector3.new(512, 10, 512),
CFrame = CFrame.new(Vector3.new(0, -5, 0)),
Color = Color3.new(0, 0, 0),
})
end
-- Text displayed when paused
local PauseText = Roact.Component:extend("PauseText")
function PauseText:render()
return Roact.createElement("TextLabel", {
Text = "Paused",
TextColor3 = Color3.new(255, 255, 255),
Font = Enum.Font.Arcade,
TextSize = 70,
Position = UDim2.new(0.5, 0, 0.5, 0),
})
end
-- Connects to UserInputService when created, disconnects when removed.
local InputConnector = Roact.Component:extend("InputConnector")
function InputConnector:render()
local function inputBeganCallback(input)
if self.props.onInputBegan then
self.props.onInputBegan(input)
end
end
local function inputChangedCallback(input)
if self.props.onInputChanged then
self.props.onInputChanged(input)
end
end
local function inputEndedCallback(input)
if self.props.onInputEnded then
self.props.onInputEnded(input)
end
end
return Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputBegan,
callback = inputBeganCallback,
}, {
Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputChanged,
callback = inputChangedCallback,
}, {
Roact.createElement(ExternalEventConnection, {
event = UserInputService.InputEnded,
callback = inputEndedCallback,
})
})
})
end
local KEYBOARD_KEY_MAPPING = {
[Enum.KeyCode.Left] = "left",
[Enum.KeyCode.A] = "left",
[Enum.KeyCode.Right] = "right",
[Enum.KeyCode.D] = "right",
[Enum.KeyCode.Up] = "up",
[Enum.KeyCode.W] = "up",
[Enum.KeyCode.P] = "pause",
}
local function getPlayerInput(input)
if input.UserInputType ~= Enum.UserInputType.Keyboard then
return nil
end
return KEYBOARD_KEY_MAPPING[input.KeyCode]
end
local KeyboardInputConnector = RoactRodux.connect(function(store)
local function onInputBegan(input)
local playerInput = getPlayerInput(input)
if playerInput then
store:dispatch({
type = "SetPlayerInput",
playerInput = playerInput,
value = 1,
})
end
end
local function onInputEnded(input)
local playerInput = getPlayerInput(input)
if playerInput then
store:dispatch({
type = "SetPlayerInput",
playerInput = playerInput,
value = 0,
})
end
end
return {
onInputBegan = onInputBegan,
onInputEnded = onInputEnded,
}
end)(InputConnector)
local RenderSteppedConnector = Roact.Component:extend("RenderSteppedConnector")
function RenderSteppedConnector:render()
return Roact.createElement(ExternalEventConnection, {
event = RunService.RenderStepped,
callback = self.props.onStepped,
})
end
-- Connect RenderSteppedConnector to actions dispatched to the store
RenderSteppedConnector = RoactRodux.connect(function(store)
return {
onStepped = function(delta)
store:dispatch({
type = "GameStep",
delta = delta,
inputs = store:getState().playerInputs,
})
end
}
end)(RenderSteppedConnector)
--[[
Only renders the given component if the given route is the current one.
The component is passed as a prop so that we don't worry about its
descendants until they're relevant.
]]
local function PauseRoute(props)
-- Injected by RoactRodux
local paused = props.paused
-- Provided by parent component
local match = props.match
local component = props.component
if paused == match then
return Roact.createElement(component)
end
end
PauseRoute = RoactRodux.connect(function(store)
local state = store:getState()
return {
paused = state.paused
}
end)(PauseRoute)
-- Package everything up in Workspace.
local App = Roact.Component:extend("App")
function App:render()
return Roact.createElement("Folder", {}, {
Player = Roact.createElement(PlayerShip),
PlayerCamera = Roact.createElement(PlayerCamera),
GameBackground = Roact.createElement(GameBackground),
Asteroids = Roact.createElement(Asteroids),
KeyboardInputConnector = Roact.createElement(KeyboardInputConnector),
RenderSteppedConnector = Roact.createElement(PauseRoute, {
match = false,
component = RenderSteppedConnector,
}),
})
end
-- Package everything up in PlayerGui.
local Ui = Roact.Component:extend("Ui")
function Ui:render()
return Roact.createElement("ScreenGui", nil, {
PauseText = Roact.createElement(PauseRoute, {
match = true,
component = PauseText,
})
})
end
local function main()
RunService.Stepped:Wait()
local store = Rodux.Store.new(reducer)
local app = Roact.createElement(RoactRodux.StoreProvider, {
store = store
}, {
App = Roact.createElement(App)
})
local ui = Roact.createElement(RoactRodux.StoreProvider, {
store = store
}, {
Ui = Roact.createElement(Ui)
})
Roact.reify(app, Workspace, "Roact-asteroids")
Roact.reify(ui, PlayerGui, "Roact-asteroids-ui")
end
return main