Notice
Placement Service is currently going through a complete overhaul of it’s systems and features to improve code readability, performance, ease of use, and improving the overall modularity of it’s usage. Be prepared for every current feature to be deprecated (or modified in some way) in later versions.
How to use Placement Service
Placement Service
Current version: 1.6.2Chapters
- The Initial Setup
- Creating hitboxes
- Using Placement Service
- Customizing Placement
- Limitations
- Extra Info
Before I go any further, you will need to get Placement Service. Once you have that, you can begin the tutorial.
Other Resources
Official github repository
Official documentationAs of version 1.5.6, this module was renamed from “Placement Module V3” to “Placement Service”.
Initial Setup
The first thing you need to do is make sure you have your game setup to use the module. You will need at least one plot to place down objects as well as a Folder
or Model
to hold the placed objects in. I like to put a Folder
located in the plot named something similar to itemHolder
or tycoonItems
.
NOTICE - Your plot size must be a multiple of your grid size. The grid size is the number of units your model will move by. In our case, the unit we’re using is studs. You will get a warning in the output otherwise.
You will also need to make three folders located in ReplicatedStorage
. One for remotes, one for models, and one for modules.
Ungroup the module and place it in modules
. You can leave models
alone but do create a RemoteFunction
called requestPlacement
. Place this in remotes
.
You will also need a Script
in ServerScriptService
to handle the server placement. This is because the module is run entirely on the client.
The final step is to add a way to start placement. In this tutorial, I will be using UI as it’s going to be the easiest way to do it. Just add a ScreenGUi
with a TextButton
. You will also need a LocalScript
in the button. This is what I have:
That should be it for the setup.
Creating hitboxes
Creating custom hitboxes is relatively simple. You’ll need a model to work with before moving on. You should already know how to construct models as this is not a tutorial on that and I will not go into detail about that here. Assuming you have a model ready, you can simply scale a part around the object creating the ‘hitbox’ for it. Then you can place that part in the model making it a child of that model. You now need to set this newly created part to the PrimaryPart
of said model. Select the model using the cursor and in properties, you should see a option for the PrimaryPart
. Click it and you will notice that your cursor has changed it’s icon. You can now select the part you want to be the PrimaryPart
in the workspace. You will probably want to lower the transparency of the PrimaryPart
as it now covers the model. You should have something that looks similar to this now:
TIP
When building the models PrimaryPart
/hitbox, you may want to put the grid texture on the plot your working on. This will help with making sure the model snaps to the grid. As long as it snaps to the grid while building, it should while placing. You can also set the snapping (located in the model tab) to whatever your grid unit will be.
Now you can move that model to the models
folder.
Using Placement Service
The next step is to open the LocalScript
we created earlier. Define variables for Players
as well as ReplicatedStorage
.
local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
We are going to need the mouse so we will also declare variables for the LocalPlayer
and the Mouse
object.
local player = players.LocalPlayer
local mouse = player:GetMouse()
It is good practice to also get references to the RemoteFunction
and TextButton
instances since we will be using them. This is not required.
local remote = replicatedStorage.remotes:WaitForChild("requestPlacement")
local button = script.Parent
The most important variable we need to define is one that returns the modules contents.
local placementService = require(replicatedStorage.modules:WaitForChild("PlacementService"))
Before you can use any of the functions in the module, we need to give it some information. We do this using the new()
function. You call the function like this: local placementInfo = placementService.new()
. The new function has multiple parameters you need to pass into it in order for it to work.
- int Grid size
- instance Item location
- Enum Rotate key keycode
- Enum Terminate/Cancel key keycode
- Enum Raise floor key keycode
- Enum Lower floor key keycode
- Enum Xbox Rotate keycode (has internal default if not input)
- Enum Xbox Terminate/Cancel key keycode (has internal default if not input)
- Enum Xbox Raise floor keycode (has internal default if not input)
- Enum Xbox Lower floor keycode (has internal default if not input)
- Instance(s) All objects input will be ignored by the mouse
Once you input those parameters you should have something like this:
local placementInfo = placementService.new(
2,
replicatedStorage.models,
Enum.KeyCode.R, Enum.KeyCode.X, Enum.KeyCode.U, Enum.KeyCode.L,
Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonX, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadDown,
objectA, objectB... -- EXAMPLE OBJECTS - NOT REQUIRED
)
Whenever you need to call a function on the module, you should use this new placementInfo
variable
. So far, you should have a script that looks similar to this:
local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
local player = players.LocalPlayer
local mouse = player:GetMouse()
local remote = replicatedStorage.remotes:WaitForChild("requestPlacement")
local button = script.Parent
local placementService = require(replicatedStorage.modules:WaitForChild("PlacementService"))
local placementInfo = placementService.new(
2,
replicatedStorage.models,
Enum.KeyCode.R, Enum.KeyCode.X, Enum.KeyCode.U, Enum.KeyCode.L,
Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonX, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadDown
)
Now, this won’t do anything yet. Before we continue, we need to add in some Events
or more formally known as RBXScriptSignals
. We only need two of them. One to listen for the the player to click the button and one to listen for a mouse click.
button.MouseButton1Click:Connect(function()
end)
mouse.Button1Down:Connect(function()
end)
We are going to activate placement when the player clicks the button and request to place down the object when we click the mouse. Before this, unless you are planning on using this without a plot, remove the noPlotActivate()
function in the module. This is to prevent exploiters from using it. To activate placement, we invoke the function activate()
on the placementInfo variable
and not the module reference. For this, make sure you are using a :
and not a .
to invoke this function. The parameters it takes are listed below:
- string Name of the model
- instance Item holder location (folder where the model will be placed)
- instance Plot location
- bool Toggles stacking
- bool Rotation type - If the model can rotate around 360 degrees or if it just rotates x amount of degrees back and fourth.
- bool Toggles auto-placement (set this to false for now)
You should have something that looks like this now:
button.MouseButton1Click:Connect(function()
placementInfo:activate("Fence", workspace.base.itemHolder, workspace.base, true, false, false)
end)
Now you should have a working “move around object system”. To make this a placement system, we need to call one last function. When we click the mouse, we want to send a request to the server to place the object. We can use the method requestPlacement()
on the placement variable
. This function takes two parameters. One for the remote event and one for the function you want to call on placement (optional). I will skip the callback for now and instead just input the remote.
mouse.Button1Down:Connect(function()
placementInfo:requestPlacement(remote)
end)
This is all we need to do for the client. Your client code should look like this now:
local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
local player = players.LocalPlayer
local mouse = player:GetMouse()
local remote = replicatedStorage.remotes:WaitForChild("requestPlacement")
local button = script.Parent
local placementService = require(replicatedStorage.modules:WaitForChild("PlacementService"))
local placementInfo = placementService.new(
2,
replicatedStorage.models,
Enum.KeyCode.R, Enum.KeyCode.X, Enum.KeyCode.U, Enum.KeyCode.L,
Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonX, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadDown
)
button.MouseButton1Click:Connect(function()
placementInfo:activate("Fence", workspace.base.itemHolder, workspace.base, true, false, false)
end)
mouse.Button1Down:Connect(function()
placementInfo:requestPlacement(remote)
end)
If you notice, we still have a “moving objects with mouse simulator” here. To fix this, we need to add some code to our server script in ServerScriptService
. This script is included with the module and can be found in the API script. The only thing you need to know in this script is where the place
function is invoked from. On the very last line, you will see the remote function we created is what invokes/calls the function. You may have to change the location and name of the remote depending on how you followed this tutorial (if your remote is named different or is in a different location).
Server Code
local replicatedStorage = game:GetService("ReplicatedStorage")
-- Ignore the top three functions
local function checkHitbox(character, object, plot)
if not object then return false end
local collisionPoints = workspace:GetPartsInPart(object.PrimaryPart)
-- Checks if there is collision on any object that is not a child of the object and is not a child of the player
for i = 1, #collisionPoints, 1 do
if not collisionPoints[i].CanTouch then continue end
if not (not collisionPoints[i]:IsDescendantOf(object) and not collisionPoints[i]:IsDescendantOf(character)) and not (collisionPoints[i] == plot) then continue end
return true
end
return false
end
-- Checks if the object exceeds the boundries given by the plot
local function checkBoundaries(plot: BasePart, primary: BasePart): boolean
local pos: CFrame = plot.CFrame
local size: Vector3 = CFrame.fromOrientation(0, primary.Orientation.Y*math.pi/180, 0)*primary.Size
local currentPos: CFrame = pos:Inverse()*primary.CFrame
local xBound: number = (plot.Size.X - size.X)
local zBound: number = (plot.Size.Z - size.Z)
return currentPos.X > xBound or currentPos.X < -xBound or currentPos.Z > zBound or currentPos.Z < -zBound
end
local function handleCollisions(character: Model, item, collisions: boolean, plot): boolean
if not collisions then item.PrimaryPart.Transparency = 1; return true end
local collision = checkHitbox(character, item, plot)
if collision then item:Destroy(); return false end
item.PrimaryPart.Transparency = 1
return true
end
--Ignore above
-- Edit if you want to have a server check if collisions are enabled or disabled
local function getCollisions(name: string): boolean
return true
end
local function place(player, name: string, location: Instance, prefabs: Instance, cframe: CFrame, plot: BasePart)
local collisions = getCollisions(name)
local item = prefabs:FindFirstChild(name):Clone()
item.PrimaryPart.CanCollide = false
item:PivotTo(cframe)
if plot then
if checkBoundaries(plot, item.PrimaryPart) then
return
end
item.Parent = location
return handleCollisions(player.Character, item, collisions, plot)
end
return handleCollisions(player.Character, item, collisions, plot)
end
replicatedStorage.remotes.requestPlacement.OnServerInvoke = place
Now if you’ve done everything correctly, it should work!
Before I move on, there are some built in functions that I will go over.
void placementInfo:noPlotActivate(string objectName, obj placedObjectsLocation, bool smartRotation, bool autoPlace)
- Same as the regular activate except it doesn’t require a plot.
void placementInfo:terminate()
- Cancels placement
void placementInfo:pauseCurrentState()
- Pauses the current state of the model
void placementInfo:resume()
- Resumes the current state of the model.
void placementInfo:editAttribute(string attributeName, var input)
- Changes the given attribute value based off of your input.
void placementInfo:haltPlacement()
- Stops any automatic placement from running
string placementInfo:getCurrentState()
- Returns the current state of the model
I will briefly go over mobile support now. The module doesn’t handle any functions with mobile, but does give you the ability to handle it on your own. What this means is to rotate the object, you have to invoke the action as appose to PC where the module handles this internally. This is because the module is designed to be as customizable as possible and requires you to use UI to trigger these actions. I didn’t want to have a single template UI that everyone has to use so I am trading ease of use for flexibility. The module does include a UI template, however it does not require you to use it. You can customize the UI as much as you’d like. Just make sure the UI for mobile is placed into the original location after. You can figure out if the user is playing on mobile by using the function placementInfo:getPlatform()
. If it returns the string “Mobile”, the user is on mobile. You can access the UI by saying: placementInfo.MobileUI
. You can then detect input on the UI you have and use the functions placementInfo:lower()
, placementInfo:raise()
, placementInfo:rotate()
, placement:terminate()
, and placementInfo:requestPlacement()
to handle those actions.
Example Code
-- Assume necessary variables are declared above ^
local function placementf()
placement:requestPlacement(place)
if placementInfo:getCurrentState() == "inactive" and not placementInfo:getPlatform() == "Mobile" then
contextActionService:UnbindAction("place")
end
end
local function raise()
placementInfo:raise()
end
local function cancel()
placementInfo:terminate()
end
local function rotate()
placementInfo:rotate()
end
local function lower()
placementInfo:lower()
end
local function startPlacement()
if placementInfo:getPlatform() ~= "Mobile" then
contextActionService:BindAction("place", placementf, false, Enum.UserInputType.MouseButton1, Enum.KeyCode.ButtonR1)
end
placementInfo:activate(model.Name, itemHolder, plot, true, false, false)
end
placementInfo.MobileUI.place.MouseButton1Click:Connect(placementf)
placementInfo.MobileUI.raise.MouseButton1Click:Connect(raise)
placementInfo.MobileUI.lower.MouseButton1Click:Connect(lower)
placementInfo.MobileUI.cancel.MouseButton1Click:Connect(cancel)
placementInfo.MobileUI.rotate.MouseButton1Click:Connect(rotate)
There are three things I skipped earlier that I will go over now. Those are autoPlacement
, callbacks
, and events/signals.
Auto placement, if set to true when invoking the activate
function, will make it so when you click to place the object down, it will automatically start placing as fast as you have it set to (placementCooldown determines this. See below for details). The first thing you will notice, is the fact that the placement doesn’t automatically stop. This feature was added so you could hold the mouse button down to place multiple objects, however the module doesn’t only limit it for that purpose, so it doesn’t stop placement automatically. Instead, you have to use the placement:haltPlacement()
function to stop placement when you want .
As for callbacks, when you request a placement from the server, you have the option to invoke a function on placement. When you call placementInfo:requestPlacement()
, after you input the remote, you can optionally input a function as a callback.
local placementService = require(replicatedStorage.modules:WaitForChild("PlacementService"))
local placementInfo = placementService.new(
2,
replicatedStorage.models,
Enum.KeyCode.R, Enum.KeyCode.X, Enum.KeyCode.U, Enum.KeyCode.L,
Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonX, Enum.KeyCode.DPadUp, Enum.KeyCode.DPadDown
)
local function callback()
print("An object was just placed")
end
mouse.Button1Down:Connect(function()
placementInfo:requestPlacement(remote, callback)
end)
--[[
You can also use callbacks like this:
placementInfo:requestPlacement(remote, function()
-- code
end)
]]
Placement Service also has it’s own set of signals that can be used to trigger certain events after an “event” occurs while placing. Using the signals is as easy as using any other signal you’ve used before. Simply say placementInfo.SIGNAL:Connect(function() -- code end)
. Here’s a list of all the signals the module offers:
- void placementInfo.Activated
- void placementInfo.Placed
- void placementInfo.Rotated
- void placementInfo.Terminated
- obj collidedObject placementInfo.Collided
- bool direction placementInfo.ChangedFloors (true = up, false = down)
Keep in mind that before you attempt to use these signals, you need to make sure that PreferSignals
is set to true. It is true by default, just keep in mind that this disables the callback feature (vice versa if set to false).
Customizing Placement
Now that you have a working placement system, it’s time to configure it to make it your own! If you click on the module, you will see it has a list of attributes you can edit. They are all documented in the module, but I’ll list them here as well:
bools
- bool AngleTilt - Toggles if you want the object to tilt when moving (based on speed)
- bool InvertAngleTilt - Inverts the direction of the angle tilt
- bool Interpolation - Toggles if you want to have the model interpolate when moving (smooth movement)
- bool MoveByGrid - Toggles if you want the model to move by a grid or not
- bool Collisions - Toggles if the module will detect collisions or not
- bool BuildModePlacement - Toggles if you want to be able to continually place objects until canceled by the user manually
- bool CharacterCollisions - Toggles character collisions (Requires “Collisions” to be set to true)
- bool DisplayGridTexture - Toggles if you want to display a grid texture when placing a model
- bool SmartDisplay - Toggles if the texture displayed will be scaled to fit the grid size. It is recommended you set this to false unless your grid size is less than 5 studs (requires displayGridTexture to be true).
- bool EnableFloors - Toggles if you want to be able to change floors while placing
- bool TransparentModel - Toggles if the model will appear transparent while placing
- bool InstantActivation - Changes if the model will glide to the mouse position or not (on activation)
- bool IncludeSelectionBox - If you want a selection box to be visible while placing
- bool GridFadeIn - If you want the grid to fade in when activating placement
- bool GridFadeOut - If you want the grid to fade out when ending placement
- bool AudibleFeedback - Toggles sound feedback on placement
- bool PreferSignals - Controls if you want to use signals or callbacks
- bool RemoveCollisionsIfIgnored - Toggles if the model itself will be transparent
- bool UseHighlights - If you want to use highlights instead of selection boxes
Color3
- Color3 CollisionColor3 - The color of the hitbox when collision is detected
- Color3 HitboxColor3 - The color of the hitbox in any non collision state; any natural state.
- Color3 SelectionBoxColor3 - The color of the selection box (IncludeSelectionBox much be set to true)
- Color3 SelectionBoxCollisionColor3 - The color of the selection box when collision is detected
Integers
- int MaxHeight - The max height one the Y axis the model can move to
- int FloorStep - The number of studs the model will move up/down when switching floors
- int RotationStep - The number of degrees the model will rotate
- int GridTextureScale - How large the StudsPerTileU/V is displayed (SmartDisplay must be set to false)
- int MaxRange - How far in studs the model can be away from the character while still being able to place.
Numbers/Floats
- Number AngleTiltAmplitude - How much the object will tilt when moving. 0 = min, 10 = max
- Number HitboxTransparency - The transparency of the hitbox when placing
- Number TransparencyDelta - The transparency of the model itself when placing (TransparentModel must be true)
- Number LerpSpeed - speed of interpolation. 0 = no interpolation, 0.9 = major interpolation
- Number PlacementCooldown - The cooldown which the user has to wait before placing another object
- Number TargetFPS - The target constant FPS [IT IS RECOMMENEDED TO LEAVE THIS AT 60]
- Number LineThickness - How thick the line of the selection box is (IncludeSelectionBox must be set to “true”)
- Number LineTransparency - How transparent the line of the selection box is (IncludeSelectionBox must be set to “true”)
- Number AudioVolume - Volume of the sound feedback (AudibleFeedback must be set to true)
Cross platform
- bool HapticFeedback - If you want a controller to vibrate when placing objects
- number HapticVibrationAmount - How large the vibration is when placing objects (value from 0, 1)
Other
- string GridTextureID - ID of the texture you want to display on the plot (DisplayGridTexture must be true)
- string SoundID - ID of the sound played on Placement (requires audibleFeedback to true)
- string Version - Has no functionality. Simply displays the version.
One other thing you can do to limit collisions of parts, is to toggle CanTouch
to false. Any parts with this settings set to false will not be detected by the collision function.
Limitations
Now, as much as I’d like to say this is the perfect placement module, I just can’t. One of the reasons being is this module is made specifically for sandbox tycoons, not open world games (although it can be used for open world games as of version 1.5.0 due to the noPlotActivate() function, but it may not work perfectly there).
Extra Info
Thank you for reading through this tutorial. I hope you found this helpful! If you didn’t, please let me know what I should modify about the tutorial and or the module (I am open to criticisms). You don’t have to give credit to use my module, though it is appreciated if given as this module has taken hundreds of hours to develop and polish. Here is a demo video:
Here is the demo place to test the module out.
Here is the copyable demo place
Enjoy the module!
Module Version Used - V1.6.2
Current version logs
2023-05-24 V1.6.2 - Details
Module changes
- Fixed bug with raising and lowering floors
- Fixed server code bug
- Removed newly deprecated raycast code
- Minor code improvements