How to use Placement Service

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.2

Welcome to the complete guide on using Placement Service. Placement Service is a fully functional placement system kit that makes it easy to create a polished placement system in minutes!

Chapters

  • 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 documentation

As 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.
itemthing

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.
folders

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:
Capture
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:
capture2.PNG

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

183 Likes

It looks pretty good, I’ll check it out later! How do you make the grid appear?

8 Likes

It’s just a texture the module applies to the plot.

2 Likes

I keep getting this warning.

The object that the model is moving on is not scaled correctly. Consider changing it.

3 Likes

Its pretty good! However, one bug I found is that exploiters are able to re-size their base and place off it. Checking where the item is placed on the server would fix that. https://gyazo.com/fb42ce9f6888b27ed1cde65a62b9aa82

6 Likes

ah ok. Will add that to the server code. Thank you for letting me know that. Ok updated

2 Likes

Is your plot size a multiple of the gridsize? It has to be to work. It does that when your plot is not.

3 Likes

How do I change the plot to be the grid size?

Edit: Nvm I read your reply wrong.

2 Likes

How can I make it so only a certain person can place on their plot?

2 Likes

This would be handled by a plot system. Since you cannot make the model leave the plot your given, assigning a plot to each player would fix this problem. This is not handled by the module.

1 Like

@zblox164

Suggestion: Make it so then we don’t have to add an up floor button and lower floor button.

4 Likes

what do you mean by that? Why would you need that?

How can I remove a placed block?

This is a separate feature which is not handled by the module. To implement that feature you will need to research this on your own. I also do have a tutorial series on Youtube on making a sandbox tycoon. You can find my channel by searching for my roblox username on YouTube.

2 Likes

Thank you very much friend help me enough although I would like there to be something to put color to the object

:+1:

robloxapp-20201024-2255349.wmv (8.0 MB)

2 Likes

All I needed to do was add a Wait() and I added 30+ more lines of code.

Nevermind. I fixed it and now you can disable the building part of it !!

Sorry for the late reply! I have come back and I need to use this module again. By that you would think that a park doesn’t have 5 floors right? Like if I were to make a Park Simulator. I wouldn’t have 5 floors. Just the one floor which is the grass/ground.

The module includes this feature via keybinds.

1 Like

Hello! How can i do a plot (place for placing models) that is made from few other parts? Like: i want to do an expand plot system where I can expand my placing model area (not using size option of part), but i don’t know how to do this. Can someone help me?