Is there a test place you can show me? I’d like to see the behavior in action. We could also chat in game / group until we have a solution others could use.
local WALL_THICKNESS = 0.4
local function getAdditionalLength(angleBetweenWalls)
return math.tan(angleBetweenWalls / 2) * WALL_THICKNESS / 2
end
local function getAngle(wallDirA, wallDirB)
return math.acos(wallDirA:Dot(wallDirB))
end
Good news! Last night we got the last script posted here working and we also added a couple more features. It now handles both halves of each wall (inner / outer or in other words left / right) differently so that when coloring them they don’t glitch the graphics where they overlap. In addition, we discovered that when the smallest angle between the walls is less than 90 degrees, the walls can’t be brought together to form a clean point. Doing so makes the wall’s thickness stick out the other end and form an hour glass looking shape around the poll. So, one wall is chosen to form the end and the other wall stops inside of that wall. This code also handles multiple-wall intersections by resizing the left half with the right half of the wall on its left side, and the right half with the left half of the wall on its right side (forward being toward the poll).
If anyone runs across a similar issue at some point, here is the code. Sorry if the math variables are not well named, there so many related in various special ways that it became difficult to find good names for them.
local WALL_HEIGHT = 12
local WALL_OFFSET = Vector3.new(0, WALL_HEIGHT, 0)
local UP = Vector3.new(0, 1, 0)
local WALL_HALF_THICKNESS = 0.2
local abs = math.abs
local function absVector3(v3)
return Vector3.new(
abs(v3.X),
abs(v3.Y),
abs(v3.Z)
)
end
local function setSize(wall, half, length)
local delta = length - wall.length[half]
local dir = wall.dir * delta
local part = wall[half]
local offset = part.CFrame:VectorToObjectSpace(dir)
part.Size = part.Size + math.sign(delta) * absVector3(offset)
part.CFrame = part.CFrame + dir / 2
wall.length[half] = length
end
local function adjust(wall, other)
local angle = math.acos(wall.rightDir:Dot(-other.rightDir))
local wallLength, otherLength
if math.abs(angle - math.pi/2) < 0.001 then
if wall.dir:Dot(other.rightDir - wall.rightDir) > 0 then
wallLength = WALL_HALF_THICKNESS
otherLength = 0
else
wallLength = 0
otherLength = -WALL_HALF_THICKNESS
end
elseif angle > math.pi/2 then
local otherAngle = math.acos(wall.dir:Dot(other.dir))
local sign = -math.sign(wall.rightDir:Dot(other.dir))
wallLength = sign * WALL_HALF_THICKNESS / math.tan(otherAngle)
otherLength = sign * math.sqrt(wallLength^2 + WALL_HALF_THICKNESS^2)
else
wallLength = math.tan(angle / 2) * WALL_HALF_THICKNESS
otherLength = wallLength
end
setSize(wall, 'left', wallLength)
setSize(other, 'right', otherLength)
end
local POLL_KEY = '%d %d %d'
local function getWalls(polls, pos)
local key = POLL_KEY:format(pos.X, pos.Y, pos.Z)
local walls = polls[key]
if not walls then
walls = {}
polls[key] = walls
end
return walls
end
local function addToPoll(polls, poll, otherPoll, model)
local walls = getWalls(polls, poll)
local dir = (poll - otherPoll).Unit
local rightDir = dir:Cross(UP)
local right, left = model.Wall1, model.Wall2
local toRight = right.Position - left.Position
if toRight:Dot(rightDir) < 0 then
right, left = left, right
end
local wall = {
length = {
left = 0;
right = 0;
};
model = model;
right = right;
left = left;
dir = dir;
rightDir = rightDir;
}
if not next(walls) then
walls[wall] = true
return
end
local leftIsFound
local leftDot
local leftWall
local rightIsFound
local rightDot
local rightWall
for otherWall in next, walls do
local dot = dir:Dot(otherWall.dir)
local isRight = rightDir:Dot(otherWall.dir) < 0
if isRight then
if not rightIsFound or not rightDot or dot > rightDot then
rightIsFound = true
rightDot = dot
rightWall = otherWall
end
if not leftIsFound and (not leftDot or dot < leftDot) then
leftDot = dot
leftWall = otherWall
end
else
if not rightIsFound and (not rightDot or dot < rightDot) then
rightDot = dot
rightWall = otherWall
end
if not leftIsFound or not leftDot or dot > leftDot then
leftIsFound = true
leftDot = dot
leftWall = otherWall
end
end
end
adjust(wall, leftWall)
adjust(rightWall, wall)
walls[wall] = true
end
local function placeWall(polls)
local mouse = game.Players.LocalPlayer:GetMouse()
mouse.Button1Down:Wait()
local pollA = snapToGrid(mouse.Hit.p)
mouse.Button1Down:Wait()
local pollB = snapToGrid(mouse.Hit.p)
local model = wallModel:Clone()
model:SetPrimaryPartCFrame(CFrame.fromMatrix(
(pollA + pollB) / 2 + WALL_OFFSET,
(pollA - pollB).Unit,
UP
))
addToPoll(polls, pollA, pollB, model)
addToPoll(polls, pollB, pollA, model)
model.Parent = workspace
end
Would you mind telling me what is required in order to implement this wonderful system into my code. For example, is further features necessary to implement or is iterating and changing what is already provided enough?
@Ukendio Recently reached out to me asking for a bit more information about how to use this code. Acting in the interest of the Roblox community I’ve decided to revive this post to provide a bit more information. Here is a snippet from the message he sent me:
The placeWall(polls, pos) function is called by addToPoll(polls, poll, otherPoll, model). polls is a table with keys being a string of three integers separated by space, essentially a stringified Vector3. The robust definition for the string format is found in the constant variable POLL_KEY = '%d %d %d'. The value of each entry in polls is another table of walls for the given position. It is a hash table, so each key is a wall and the value is always true. The structure of a wall is found on lines 71-81:
local wall = {
length = {
left = 0;
right = 0;
};
model = model;
right = right;
left = left;
dir = dir;
rightDir = rightDir;
}
As for the arguments of addToPoll(polls, poll, otherPoll, model), polls is the same table as described above, poll is a Vector3 position, otherPoll is the same, and model is the actual Model instance for the wall. It must have at least two children, Wall1 and Wall2 as defined on line 65:
local right, left = model.Wall1, model.Wall2
These left and right portions of the wall have their sizes adjusted by adjust(wall, other) to create the optimal joint between two walls. It handles the angles between walls when adjusting and has special cases for obtuse, acute, or 180 degree angles. addToPoll looks at all the walls connected to poll location to determine what walls need to be adjusted with the addition of the new wall.
The function setSize(wall, half, length) actually adjusts the wall length. You may need to edit this function depending on your left and right wall orientations. This function shrinks/grows the specified side of the wall in the wall’s direction to the desired length. It offsets the position of the wall so that only only one side of the wall is changed and the other remains the same.
Note that addToPolls is called is called by placeWall twice, once for each poll location, so that each side of the wall is adjusted to be optimal with the other walls on each side of the new wall.
This script will work when you define the function Vector3 snapToGrid(Vector3) (possibly to round to the nearest stud), make wallModel as a model with Wall1 and Wall2 children, and polls (the argument to placeWall(polls)) as an empty table, put this script in a LocalScript in the character scripts folder, and call placeWall. You may have resizing issues with the wall halves for which you’ll have to rotate and resize them to adjust which axis is pointing in the wall’s direction.
Been looking at this as I have a similar problem. Just a heads up that the model clone never has its parent set. By default, when an object is cloned, its parent is set to nil.
Ah, yes. The code snippet was originally integrated with a larger system and I tried to modify it so it could stand alone but missed that. I’ve modified the solution to set the parent of the model.
I am wondering how I would prevent players from trying to go below a certain degree? Like, to make them unable to make an edge unrealistically sharp. My initial attempt at patching this was to destroy the wall in the adjust function, however because addtopoll(x2) essentially calls the function four times, it will error but that is not necessarily the issue. The main issues at hand is that the wall…
i. must be placed before I can check the angle of the walls that needs adjusting. And as consequence (ii)
ii. affects other walls’ compenents whereas they get 'adjusted, even when prevented a new wall to transpire.
//To give backstory (idk if this will help), I am trying to make the wall indiciate when it can’t be placed (LIVE).
In the function addToPoll after the for loop and right before the adjustments are made, I’d split the function. Instead if the angle between the leftWall and rightWall are greater than the minimum threshold, return leftWall, rightWall, and the new wall for the caller to adjust later. If either angle is less than the minimum threshold, return nothing (adding the wall to the poll failed). In the caller, if adding the wall to either poll fails (either call to addToPoll returns nothing), then don’t perform the adjustments on the left and right walls for each poll. Otherwise, continue with the adjustments and add the new wall to each poll’s list of walls. It is important that the wall being added actually has one table representing it per a poll. This is in part because the left and right neighbors are different for each poll and the directions are inverted.
Note that since the addToPoll function checks angles against the minimum threshold, the threshold will need to be visible to the function either as an argument or upvalue constant. To check the angle between the left and right walls to the new wall, check that the leftDot and rightDot variables are greater than the cosine of the minimum angle threshold. This equation stems from the fact that the dot product of two vectors is the cosine of the angle between them scales by the magnitude of both vectors. Since both are unit vectors, the dot product is simply the cosine of the angle between them. Thus, we need to compare the dot product to the cosine of the minimum threshold.
The left and right dot products are already computed and stored in the variables leftDot and rightDot. You can check that math.acos(leftDot) > threshold and math.acos(rightDot) > threshold, or find the cosine of the threshold once for both calculations (and if the threshold is unchanging, even store it as a constant).
I mean that after the for loop perform the above checks and then return the specified values. Leave adjusting the walls and adding the wall to the poll’s list of walls to the caller. This way, the caller can make sure that the up to 4 neighbor walls (2 per a poll) all meet the minimum threshold requirement before the adjustments are made and the wall added to the polls list. In placeWall you can check that the results from both calls to addToPoll are not nil and then adjust the adjacent walls and add the wall to each polls’ list of walls.
Oof, I have tried a few things for a while such as implementing this. I am sure I must have misinterpreted you along the way but here are the two alternatives I wrote
if math.acos(rightDot) > math.pi/4 then
walls[wall] = true
return adjust(wall, leftWall)
end
if math.acos(leftDot) > math.pi/4 then
walls[wall] = true
return adjust(rightWall, wall)
end
and
if math.acos(rightDot) > math.pi/4 and math.acos(leftDot) > math.pi/4 then
walls[wall] = true
return leftWall, rightwall
end
adjust(wall, leftWall)
adjust(rightWall, wall)
This however has no effect, where as it keeps ‘functioning’ despite the angle is smaller than the threshold.
if math.acos(rightDot) > math.pi/4 and math.acos(leftDot) > math.pi/4 then
walls[wall] = true
return leftWall, rightwall
end
adjust(wall, leftWall)
adjust(rightWall, wall)
I did this
if math.acos(rightDot) > math.pi/4 and math.acos(leftDot) > math.pi/4 then
walls[wall] = true
adjust(wall, leftWall)
adjust(rightWall, wall)
return leftWall, rightWall
else
return false
end
needs to be done in the parent. Otherwise one poll may think the wall is okay to be placed and do so but the second poll says the wall is invalid. The adjustments need to be made in the caller, AFTER both calls to addToPoll return that the wall is valid.
local function placeWall()
if mouseDown then
templatePole.Transparency = 0.65
local wall = wallModel
wall.Parent = workspace.Walls
wall.Name = “Cool”
wall.Wall1.Transparency = 0
wall.Wall2.Transparency = 0
wall.Parent = workspace.Walls
if wall.Hitbox.Size.X < 3.5 then
wall:Destroy()
endWallPlacement()
else
addToPoll(polls, pollA, pollB, wall)
addToPoll(polls, pollB, pollA, wall)
if addToPoll(polls, pollA, pollB, wall) then
local walls = getWalls(polls, pollA)
walls[wall] = true
adjust(wall, wall.otherWall)
end
if addToPoll(polls, pollB, pollA, wall) then
local walls = getWalls(polls, pollA)
walls[wall] = true
adjust(wall, wall.otherWall)
end
endWallPlacement()
end
mouseDown = false
end
I believe I owe you an apology. To set the stage, I’m very busy between graduate school and work. The main reason I find myself active on these forums (despite not having the time for it) is that I enjoy doing what I’m good at; I’ve been active on Roblox for 12 years and developing on it for nearly as long. When I grow tired of struggling on difficult projects for school I come here. I really shouldn’t be active on here and don’t have time to do so, so please forgive me when I do not get back to you.
Please, don’t take my non-response personally; I mean no harm and harbor no ill-intent.
I find natural language to be a highly ambiguous and at times terribly inefficient at communicating details. I also found the method I originally described to produce some nasty code, so I introduced a callback to clean it up.
local thresh = acos(math.pi/2)
local function addToPoll(polls, poll, otherPoll, model)
...
local leftIsFound
local leftDot
local leftWall
local rightIsFound
local rightDot
local rightWall
for otherWall in next, walls do
...
end
if leftDot >= thresh and rightDot >= thresh then
return function ()
adjust(wall, leftWall)
adjust(rightWall, wall)
walls[wall] = true
end
end
end
local function placeWall(polls)
...
place_A = addToPoll(polls, pollA, pollB, model)
place_B = addToPoll(polls, pollB, pollA, model)
if place_A and place_B then
place_A()
place_B()
model.Parent = workspace
else
model:Destroy()
end
end
I appreciate your help, you didn’t need to apologise! Secondly, I want to bring up that the model would be destroyed 100% of the time because it is not returning anything (probably due to that it is checking the angle of nothing thus returning nil → else model:Destroy() is executed?