Skip to content

Tutorial

NOTE THIS IS OUTDATED. PLEASE USE THE DEV FORUM TUTORIAL FOR NOW

Image title

How to use Placement Service

Placement Service

Current version: 1.6.1

Welcome to the complete guide on using Placement Service. It is a fully functional placement system kit that makes it easy to create a polished placement system in minutes! Note this tutorial was ported directly from the developer forum version. This will not cover anything that tutorial doesn't.

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.

Notice

As of version 1.5.6, this module was renamed from “Placement Module V3” to “Placement Service”.

NOTE THIS IS OUTDATED. PLEASE USE THE DEV FORUM TUTORIAL FOR NOW


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|219x51, 75%

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|303x105, 75%

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|661x452, 75%

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

local replicatedStorage = game:GetService("ReplicatedStorage")

-- Ignore the top three functions

-- Credit EgoMoose
local function checkHitbox(character, object)
    if object then
        local collided = false

        local collisionPoint = object.PrimaryPart.Touched:Connect(function() end)
        local collisionPoints = object.PrimaryPart:GetTouchingParts()

        for i = 1, #collisionPoints do
            if not collisionPoints[i]:IsDescendantOf(object) and not collisionPoints[i]:IsDescendantOf(character) then
                collided = true

                break
            end
        end

        collisionPoint:Disconnect()

        return collided
    end
end

local function checkBoundaries(plot, primary)
    local lowerXBound
    local upperXBound

    local lowerZBound
    local upperZBound

    local currentPos = primary.Position

    lowerXBound = plot.Position.X - (plot.Size.X*0.5) 
    upperXBound = plot.Position.X + (plot.Size.X*0.5)

    lowerZBound = plot.Position.Z - (plot.Size.Z*0.5)   
    upperZBound = plot.Position.Z + (plot.Size.Z*0.5)

    return currentPos.X > upperXBound or currentPos.X < lowerXBound or currentPos.Z > upperZBound or currentPos.Z < lowerZBound
end

local function handleCollisions(char, item, c)
    if c then
        if not checkHitbox(char, item) then
            item.PrimaryPart.Transparency = 1

            return true
        else
            item:Destroy()

            return false
        end
    else
        item.PrimaryPart.Transparency = 1

        return true
    end
end

--Ignore above

local function place(plr, name, location, prefabs, cframe, c, plot)
    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(plr.Character, item, c)
    else
        return handleCollisions(plr.Character, item, c)
    end
end

replicatedStorage.remotes.functions.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 placement:noPlotActivate(string objectName, obj placedObjectsLocation, bool smartRotation, bool autoPlace) - Same as the regular activate except it doesn't require a plot.

void placement:terminate() - Cancels placement

void placement:pauseCurrentState() - Pauses the current state of the model

void placement:resume() - Resumes the current state of the model.

void placement:editAttribute(string attributeName, var input) - Changes the given attribute value based off of your input.

void placement:haltPlacement() - Stops any automatic placement from running

string placement: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 it's 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.

-- 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)
    else
        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)
    end

    placementInfo:activate(model.Name, itemHolder, plot, true, false, false)
end

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 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 need to.

As for callbacks, when you request a placement from the server, you have the option to invoke a function on placement. When you call placement: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)
]]

As of version 1.5.0, the placement module now includes 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 placement.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 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

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

Current version logs

2022-08-19 V1.5.8 - Details:

Module changes

  • Migrated to use raycasts instead of the mouse object
  • Improved position accuracy
  • Improved stacking
  • Improved collision detection for interpolation and angle tilting
  • Positions are now relative to plots rotation
  • Added new parameter in the new() constructor to allow for ignored objects
  • Added new attribute: RemoveCollisionsIfIgnored
  • Minor improvements and fixes