PcoWSkbVqDnWTu_dm2ix
We use cookies on this site to enhance your user experience

Top Down Action: Controls

Top Down Action: Controls

Jul 30 2018, 3:10 PM PST 10 min

Now let’s make the player’s character always face the mouse cursor. In StarterPlayerScripts| StarterPlayerScripts, add a new LocalScript and call it CharacterControls.

TDS_CharacterControlsScript.png

We will use a BodyGyro|BodyGyro to make the character face the mouse. A BodyGyro|BodyGyro will attempt to keep its parent at a set orientation. BodyGyros are part of a group of instances called BodyMover|BodyMovers, all of which apply forces in-game.

First, let’s create a function that will create a BodyGyro|BodyGyro in a player’s character. In the CharacterControls LocalScript add the following code:

local gyro = nil

local function onCharacterAdded(character)
	local torso = character:WaitForChild('Torso')
	gyro = Instance.new('BodyGyro')
	gyro.P = 50000
	gyro.MaxTorque = Vector3.new(0, 10000, 0)
	gyro.Parent = torso
end

We will need to manipulate the gyro outside of the function, so we first have to declare it outside the scope of the function. Then, we declare a function which takes a player character as an argument. The character model might not have fully loaded by the time this function is called, so it is important to use Instance/WaitForChild|WaitForChild when accessing the Torso part to make sure it exists.

Next, we create a BodyGyro as a child of the character’s torso and tweak the values of the gyro. The BodyGyro/P|P property sets how aggressively the BodyGyro tries to reach it’s goal orientation. The default value is too low to sufficiently turn the character, so we increase it to 50000 to make sure it is quick and responsive. We also need to set the maximum torque of the BodyGyro to a sufficiently high number, but only in the Y axis as we do not want to force rotation in the other two.

Now that we have a function to insert a BodyGyro into the player’s character, we need to bind that function to when the character is added to the game.

local player = game.Players.LocalPlayer
local gyro = nil

local function onCharacterAdded(character)
	local torso = character:WaitForChild('Torso')
	gyro = Instance.new('BodyGyro')
	gyro.P = 50000
	gyro.MaxTorque = Vector3.new(0, 10000, 0)
	gyro.Parent = torso
end

while not player.Character do wait() end
onCharacterAdded(player.Character)
player.CharacterAdded:connect(onCharacterAdded)

When a player joins the game for the first time, a character will automatically spawn. This script will run at the same time, so there is no guarantee whether this script will run before the character finishes loading. So, we add a small loop to wait until the player’s Character exists before we bind the onCharacterAdded function.

Tracking the Mouse

Now we need to write a function to orient the BodyGyro|BodyGyro to face the mouse. To make sure the motion is smooth, we will bind the function to the render step (much like we did with the Camera).

local player = game.Players.LocalPlayer
local gyro = nil
local mouse = player:GetMouse()
mouse.TargetFilter = game.Workspace.LevelGeometry
local runService = game:GetService('RunService')

local function onCharacterAdded(character)
	local torso = character:WaitForChild('Torso')
	gyro = Instance.new('BodyGyro')
	gyro.P = 50000
	gyro.MaxTorque = Vector3.new(0, 10000, 0)
	gyro.Parent = torso
end

local function onRenderStep()
	if gyro then
		local mouseHit = mouse.Hit.p
		gyro.CFrame = CFrame.new(player.Character.Torso.Position, mouseHit)
	end
end

while not player.Character do wait() end
onCharacterAdded(player.Character)
player.CharacterAdded:connect(onCharacterAdded)

runService:BindToRenderStep('TrackMouse', Enum.RenderPriority.Input.Value, onRenderStep)

We first need to store the player’s mouse in a variable. We then want to make sure the mouse ignores all of the objects in the level (except for the terrain beneath). We can do this by setting the Mouse/TargetFilter|TargetFilter of the mouse to the LevelGeometry folder in the Workspace.

In our render step function, we simply check to see if the gyro exists to cover cases where the player’s character hasn’t spawned in yet. If the gyro exists, then we get the position in the game where the mouse is pointing, and then set the CFrame of the body gyro to point at that location.

Binding the function to the render step is done nearly identically to when we bound our custom Camera, the only difference being that we want this update to happen on the Input phase of the step, not the Camera phase.

Checking for Mouse Errors

We need to add an error check in case the mouse is positioned in a bad spot. This is particularly concerned when the game first launches when the level has not finished loading. In such cases, it’s possible for the mouse to be pointing at something off of the level which will make the position values of the mouse not a number.

To check for a bad mouse position, we’ll define a small helper function to check if a number is actually a number. A simple Lua trick is to compare something to itself. Bad numbers will actually return false if you try this comparison.

local player = game.Players.LocalPlayer
local gyro = nil
local mouse = player:GetMouse()
mouse.TargetFilter = game.Workspace.LevelGeometry
local runService = game:GetService('RunService')

local function onCharacterAdded(character)
	local torso = character:WaitForChild('Torso')
	gyro = Instance.new('BodyGyro')
	gyro.P = 50000
	gyro.MaxTorque = Vector3.new(0, 10000, 0)
	gyro.Parent = torso
end

local function isnan(x) return x ~= x end

local function onRenderStep()
	if gyro then
		local mouseHit = mouse.Hit.p
		if not (isnan(mouseHit.X) or isnan(mouseHit.Y) or isnan(mouseHit.Z)) then
			gyro.CFrame = CFrame.new(player.Character.Torso.Position, mouseHit)
		end
	end
end

while not player.Character do wait() end
onCharacterAdded(player.Character)
player.CharacterAdded:connect(onCharacterAdded)

runService:BindToRenderStep('TrackMouse', Enum.RenderPriority.Input.Value, onRenderStep)

Now we are checking if the mouse is actually pointing somewhere on the level. If we don’t do this, the game could break.

Prevent Jumping and Climbing

The last thing we need to do for the controls is to prevent the character from jumping and climbing. We don’t have any ladders or pits in our game, so these two actions are unnecessary.

local player = game.Players.LocalPlayer
local gyro = nil
local mouse = player:GetMouse()
mouse.TargetFilter = game.Workspace.LevelGeometry
local runService = game:GetService('RunService')

local function onCharacterAdded(character)
	local torso = character:WaitForChild('Torso')
	gyro = Instance.new('BodyGyro')
	gyro.P = 50000
	gyro.MaxTorque = Vector3.new(0, 10000, 0)
	gyro.Parent = torso
	
	local humanoid = character:WaitForChild('Humanoid')
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
	humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false)
end

local function isnan(x) return x ~= x end

local function onRenderStep()
	if gyro then
		local mouseHit = mouse.Hit.p
		if not (isnan(mouseHit.X) or isnan(mouseHit.Y) or isnan(mouseHit.Z)) then
			gyro.CFrame = CFrame.new(player.Character.Torso.Position, mouseHit)
		end
	end
end

while not player.Character do wait() end
onCharacterAdded(player.Character)
player.CharacterAdded:connect(onCharacterAdded)

runService:BindToRenderStep('TrackMouse', Enum.RenderPriority.Input.Value, onRenderStep)

Now when the character spawns in the game, both the Jumping and Climbing states will be disabled which will prevent the character from being able to jump or climb.

TDS_Controls.png