How to use Placement Service
- The Initial Setup
- Creating hitboxes
- Using Placement Service
- Customizing Placement
- Extra Info
Before I go any further, you will need to get Placement Service. Once you have that, you can begin the tutorial.
As of version 1.5.6, this module was renamed from “Placement Module V3” to “Placement Service”.
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
Model to hold the placed objects in. I like to put a
Folder located in the plot named something similar to
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
requestPlacement. Place this in
You will also need a
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 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:
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
The next step is to open the
LocalScript we created earlier. Define variables for
Players as well as
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
local player = players.LocalPlayer local mouse = player:GetMouse()
It is good practice to also get references to the
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
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).
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.
placementInfo:noPlotActivate(string objectName, obj placedObjectsLocation, bool smartRotation, bool autoPlace) - Same as the regular activate except it doesn’t require a plot.
placementInfo:terminate() - Cancels placement
placementInfo:pauseCurrentState() - Pauses the current state of the model
placementInfo:resume() - Resumes the current state of the model.
placementInfo:editAttribute(string attributeName, var input) - Changes the given attribute value based off of your input.
placementInfo:haltPlacement() - Stops any automatic placement from running
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:requestPlacement() to handle those actions.
-- 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
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).
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:
- 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 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
- 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.
- 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)
- 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)
- 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.
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).
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:
Enjoy the module!
Module Version Used - V1.6.2
Current version logs
2023-05-24 V1.6.2 - Details
- Fixed bug with raising and lowering floors
- Fixed server code bug
- Removed newly deprecated raycast code
- Minor code improvements