Yes but that’s easier said than done. It’s very easy for someone as experienced as yourself to say “that’s it”. But the people reading this tutorial are most likely not experienced enough to say that; otherwise reviewing the tutorial. Simply saying “that’s it” doesn’t help anyone move forward. Yes, you’re obviously constraining it to the boundaries of your canvas but how, not what. For example, CFrame.fromMatrix() was explained to me like this
CFrame.fromMatrix(
Vector3.new(x,y,z),
Vector3.new(r00,r10,r20),
Vector3.new(r01,r11,r21),
Vector3.new(r02,r12,r22)
)
-- is equivalent to
CFrame.new(
x,y,z,
r00,r01,r02,
r10,r11,r21,
r20,r21,r22
)
Unsurprisingly, I was still left clueless as to how this works. Adding a little bit more explanation to actually help people understand the things they’re writing rather than just copying from a website is going to help them much more than simply telling them to repeat after you. Another thing I noticed is that your tutorial is hard to follow and keep up with. A LocalScript can not access ServerScriptService yet you never specified this to clarify this for amateur scripters and rather than explaining this concept or giving them an alternative solution (like putting it in ReplicatedStorage), you left them in the dark. Another example is the very first script you wrote.
-- Server Script
-- the module script from below
local placementClass = require(game:GetService("ReplicatedStorage"):WaitForChild("Placement"))
local placementObjects = {}
local remotes = game:GetService("ReplicatedStorage"):WaitForChild("Remotes")
-- creates the server twin, stores in a table and returns the CanvasObjects property
function remotes.InitPlacement.OnServerInvoke(player, canvasPart)
placementObjects[player] = placementClass.new(canvasPart)
return placementObjects[player].CanvasObjects
end
-- finds the server twin and calls a method on it
-- note: b/c we aren't using the standard method syntax we must manually put in the self argument
remotes.InvokePlacement.OnServerEvent:Connect(function(player, func, ...)
if (placementObjects[player]) then
placementClass[func](placementObjects[player], ...)
end
end)
-- Class (Module Script)
local isServer = game:GetService("RunService"):IsServer()
local Placement = {}
Placement.__index = Placement
function Placement.new(canvasPart)
local self = setmetatable({}, Placement)
-- the part we are placing models on
self.CanvasPart = canvasPart
-- custom logic depending on if the sevrer or not
if (isServer) then
-- create a folder we'll place the objects in
self.CanvasObjects = Instance.new("Folder")
self.CanvasObjects.Name = "CanvasObjects"
self.CanvasObjects.Parent = canvasPart
else
-- initiate the twin on the server
self.CanvasObjects = initPlacement:InvokeServer(canvasPart)
end
-- we'll talk about these properties later in the post
self.Surface = Enum.NormalId.Top
self.GridUnit = 1
return self
end
return Placement
A lot of this was left unclarified and was quite hard to follow with no clear, given instructions. No prior instructions were given; for example, you never said make a folder called remotes in ReplicatedStorage; you never specified whether these were to be RemoteEvents or RemoteFunctions. Yes, working in OOP environments is a great practise to try and teach people - however, leaving them in the dark only leads to a spiral of ever-increasing problems. Thank you for your contribution, regardless.