Help refining player control after restricting movement to grid

I had some great pointers over in the Game Design Support on the best approach to restrict a players movement to x / y axis but now I have put an example together to test it I need some scripting support to improve things.

TL;DR
I have a local player script where I;

  1. Test the player is near the center of a tile
  2. If in center of a tile, let them start moving along either the X axis, or the Z axis
  3. If not in centre of tile restrict to the axis they began walking along.
    I have concentrated on keyboard only for now

But, now testing you can tell that this foresight from @EmilyBendsSpace feels required. I just don’t know where to start to adjust my script to implement it.

Have I implemented this badly? How can adjust the moveVector to add a correction?

Any scripting pointers appreciated.

4 Likes

I actually really like this idea. I think the best option would be to restart the script, putting it on one tile. Then duplicate that tile and place it perfectly alongside the other one.

You would also have to implement a script where if the player is touching the side of one tile, it lets them keep moving.

Than can be achieved by this:

Lets assume tile2 is on the left of tile1 and you’re using A to move left.

If the player is touching tile1 and also touching tile2
if pressing key A
Change player position left

Now the player would successfully be on the other tile and could function normally. I don’t script but if you know how, this should work.

1 Like

I wanted to give this a shot myself because it’s an interesting problem.

I’ve got it working, but it needs to be refined to make it feel just right. Here’s the place file if you want to check it out. The meat of it is in StarterPlayerScripts.ControlScript. It includes some comments on implementation details, as well as thoughts on how/why certain things work, and ideas for alternatives.
grid-dev-forum.rbxl (35.1 KB)

First of all, I made some superficial changes that you should be aware of. I moved the camera to be aligned with the world’s axes (so it points in the -Z, or forward, direction). I renamed one of the grid cells to “InitialCell” and all the others to “Cell”, and I renamed the script responsible for the movement to “ControlScript”, because this overrides/disables the default built-in controls.

My approach doesn’t try to limit the movement of the character itself to an Axis, but rather has a “current cell/target cell” system. At any one time there is a current and a target cell, and the character always walks towards the target cell. When it gets close enough to the target cell, it becomes the current cell. When the player is pressing the key to move left, the cell to the left of the current cell becomes the target cell. This is checked/updated once every frame. Holding down a movement key makes the character move smoothly in that direction until the key is released, even across multiple cells. The character never stops halfway between two cells because it’s always walking towards a specific cell. The character’s position in terms of which cell it’s on is always clearly defined, which is the strong point of my solution.

Hope this helps :slight_smile: Let me know if you have any questions.

1 Like

Wow. I appreciate the effort @ThanksRoBama! Interestingly it sounds like you have tackled it in similar way to an approach I noted in the other post (working out the target square from the users input). But I would have struggled to code it up. I will take a good look through the example as soon as I’m at my computer.

Thanks so much.

Thanks @RecBtw Sounds a bit like my initial idea about using ‘invisible walls’

Yes. Your idea sounds far more organized. I really want to see how this turns out.

Firstly, thanks again. I have learnt a tonne from reading through what you have done and the comments! I spent well over a week on my implementation, looking in the docs and on posts here piecing it together so it is inspiring to see this just a day later.

I don’t want to drown you in questions but some parts that are puzzling me…

getMoveDirection()

moveDirection = moveDirection + Vector3.FromNormalId(Enum.NormalId.Front)

I see Vector3.FromNormalId Constructs a new Vector3 in a particular direction and the Enum sets which side/face of a Part is used to set the direction but of which parts Front are we using here? Is it defaulting to the world?

isTryingToMove()
I know very little about raycasting but is the cellSpacing value here the length of the cell + the gap length? If the grid were constructed programatically the sensitivity to cells moving might not be an issue?

greatestAxisV3(v3)
Are you including Y axis simply for completeness?

ControlScript

Does this cause issues if Roblox is updated? I see in the console output is see Infinite yield possible on 'Players.JayRey78.PlayerScripts.ControlScript:WaitForChild("MasterControl")' Is this connected?

1 Like

Yep, it’s the “front” direction, in world space coordinates. If you wanted to find the “front” direction of a given part, you’d do

local frontDirOfPart = part.CFrame:VectorToWorldSpace(Vector3.FromNormalId(Enum.NormalId.Front))

The above gives the same result as part.CFrame.LookVector, but you can use all the other directions as well which is handy.

Yeah, that’s right. Doesn’t have to be that exact value though, since it’s only used when determining how long the ray should be when finding the target cell. If it were a few studs longer/shorter it wouldn’t break the script, since the ray is cast outwards from the center of the current cell.

Sure, then it wouldn’t ever be an issue. It most likely wouldn’t be either when building the grid/level by hand, I just wanted to make sure you weren’t getting unexpected behavior in case you didn’t realize that the rays are being cast sideways/horizontally. Since the cell parts are very thin, if one of them is just a little bit lower/higher than the cell where a ray is being cast from, the ray might miss. This is why I suggest figuring out another way to find the target cell, in case you want to add height variations to the cells or something like that.

Yeah, since there’s no way to attempt to move up/down, the Y axis of the moveDirection vector is always going to be 0 and will never be “greatest”. It’s a pretty generic function that I’ve used in several projects, so it’s not specialized for this specific use case. I don’t know if including the Y axis check makes it more or less readable.

This is a warning from the CameraScript which handles the default camera behavior. A warning is emitted because the part of CameraScript that handles virtual reality stuff wants to do something with the default ControlScript. It expects a script called “MasterControl” to be a child of the default ControlScript and uses WaitForChild to wait for it. This method has a default timeout period of 5 secs, so when it doesn’t find it after 5 seconds it throws a warning.

It’s not a serious issue, unless you plan on doing anything VR related. I’ve never had problems with any other parts of the default CameraScript even when getting this error, so you should be fine. If you’re not comfortable living with that warning, you can choose not to override the default ControlScript and instead disable it manually like in your original solution.

2 Likes

Awesome! Much kudos. I can’t wait to play around with your code some more and implement it in my game idea. Much to do still (counting moves, other team players, scoring, animating etc) but this was a massive help.

1 Like

@ThanksRoBama Is touch controls a whole can of worms here? I’m assuming this file overwrites the default touch screen controls so I’d have to build my own?

1 Like

Yes, you’ll have to implement your own version of touch controls for this to work.

Random rambly background info

If you need touch controls and want them to be as close to the default controls as possible, you might want to actually use the default controls. Problem is, you need more control than the default system lets you have.

You don't really need to read all this, but I typed it already and I'm not going to delete it now >:C

Luckily, you can copy the default control scripts when the game is running and just insert them to StarterPlayerScripts, like so:

image

Now you can modify it to your heart’s content. Buuuuuut there’s several thousand lines of code, so actually knowing it well enough to modify it to your needs is an enormous task.

Thankfully the default controls are super modular and self- contained. Sure it has all sorts of logic to calculate how the player should move with all sorts of control schemes, but it actually just outputs it in a single place. The PlayerModule is just loaded/required by the PlayerScriptsLoader, which has this single line of code:

local PlayerModule = require(script.Parent:WaitForChild("PlayerModule"))

The PlayerModule basically just loads two other modules, CameraModule and ControlModule. ControlModule deals with player controls, and has a property called moveFunction which is just set to LocalPlayer.Move. If we change the PlayerScriptsLoader to this:

local PlayerModule = require(script.Parent:WaitForChild("PlayerModule"))
PlayerModule.controls.moveFunction = function(...)
	print(...)
end

All of the controls still work, and touch control widgets appear when emulating a mobile device in Studio. Instead of moving the character though, it just prints something like "ThanksRoBama 0, 0, 0 false". Looking at the wiki page I linked for Player.Move, it expects a walkDirection Vector3 and a relativeToCamera bool.

So that’s how you can hook into the default controls and override them :smiley:

Usage example

To make it super convenient to hook into the default controls, you can delete the PlayerModule and instead just have the script loader and a custom control script (shouldn’t be called "ControlScript" though, because we don’t want to overwrite the default controls). Like so:
image

Change the script loader to hook the controls into a BindableEvent, like so:

local PlayerMoveEvent = Instance.new("BindableEvent")
PlayerMoveEvent.Name = "PlayerMove"
PlayerMoveEvent.Parent = game.Players.LocalPlayer.PlayerScripts

local PlayerModule = require(script.Parent:WaitForChild("PlayerModule"))
PlayerModule.controls.moveFunction = function(player, direction, relativeToCamera)
	PlayerMoveEvent:Fire(direction)
end
And here's an example custom control script that limits movement to the world X and Z axis:
local player = game.Players.LocalPlayer
local playerMoveEvent = player.PlayerScripts:WaitForChild("PlayerMove")

function absV3(v3)
	return Vector3.new(
		math.abs(v3.X),
		math.abs(v3.Y),
		math.abs(v3.Z)
	)
end

function greatestAxis(v3)
	local v3 = absV3(v3)
	local max = math.max(v3.X, v3.Y, v3.Z)
	if max == v3.X then
		return Enum.Axis.X
	elseif max == v3.Y then
		return Enum.Axis.Y
	else
		return Enum.Axis.Z
	end
end

function onPlayerMove(dir)
	local moveAxis = greatestAxis(dir)
	player:Move(Vector3.FromAxis(moveAxis) * dir, false)
end

playerMoveEvent.Event:Connect(onPlayerMove)

The moveFunction seems to get called every RenderStepped, so instead of calling getMoveDirection() inside a function bound to RenderStepped, you can just bind a function to the BindableEvent and it will get called just as often. The single argument that gets passed along is the desired walk direction, so getMoveDirection() is obsolute with this system.

3 Likes

Wow. Again you have gone out of your way to help and I really appreciate it.

I will see if I can piece this puzzle together! rubs hands together

1 Like

Just stopping by to say massive thanks @ThanksRoBama ! After a break from it I came back this afternoon and patched the pieces together. I uploaded and tested and mobile controls work and player is locked to the grid!

1 Like