Need help with placement system and enemy movement code for tower defense game

  1. What do you want to achieve?
    Ok, this is going to seem odd but I am having 2 issues and I would prefer to combine the issues into one post rather than make 2 separate posts. Anyways, I am at the point where I am coding the main game of my tower defense game, and I have 2 inconveniences popping up in my game. The first one, the most annoying, comes with my movement of enemies through the map. Another issue has to do with my placement system.

  2. What is the issue?
    So, issue #1:
    How my game works is I have all the waves inside a module script for spawning. I will call the waves from a main server script then I execute code in the same module script to spawn enemies on the map and move them from the start of the map to the end. I made a demo a bit ago to test some things and the code for enemy movement I made worked without issue, so I thought it would be better to reuse it as there was no issue with it. However, the issue I am experiencing can be seen in the following video:


    In the video I highlight the only turn causing an issue. Zombies will only spasm and delete like the first, or jitter like the other zombies on only the second turn. I have no idea why this occurs as in the demo I made which uses the exact same code doesn’t have any issue at all. Here is the code:

function wave(enemyType, enemyCount)
	for i = 1, enemyCount do
		spawn(function()
			local enemyClone = enemyType:Clone()
			enemyClone.HumanoidRootPart.Position = spawnPoint.Position
			enemyClone.Parent = map.Enemies
			enemyClone.Name = enemyType.Name
			local walkAnim = enemyClone.Humanoid:FindFirstChild("Walk")
			local walkAnimation = enemyClone.Humanoid:LoadAnimation(walkAnim)
			walkAnimation:Play()
			table.insert(enemiesModule.enemies, enemyClone.Name)
			enemiesRemaining.Value = enemiesRemaining.Value + 1
			for i = 1, (#trackParts:GetChildren() - 1), 1 do
				local trackPart = trackParts[i]
				local previousValue = i - 1
				local previousTrackPart = trackParts:FindFirstChild(previousValue)
				enemyClone:SetPrimaryPartCFrame(previousTrackPart.CFrame)
				local endPoint = {
					Position = trackPart.CFrame.Position
				}
				local tweenInfo = TweenInfo.new(((trackParts:FindFirstChild(i).Position - trackParts:FindFirstChild(previousValue).Position).Magnitude) / (1 + enemyClone.Humanoid.WalkSpeed), Enum.EasingStyle.Linear, Enum.EasingDirection.Out)
				local tween = tweenService:Create(enemyClone.PrimaryPart, tweenInfo, endPoint)
				tween:Play()
				tween.Completed:Wait(0.1)
			end
			local damage = enemyClone:FindFirstChild("Configuration"):WaitForChild("Damage")
			if healthValue.Value > 0 then
				healthValue.Value = healthValue.Value - damage.Value
			elseif healthValue.Value < 0 then
				healthValue.Value = 0
			end
			local animation = enemyClone.Humanoid.DeathAnim
			local anim = enemyClone.Humanoid:LoadAnimation(animation)
			anim:Play()
			anim.Stopped:Wait(0.1)
			enemyClone.Humanoid:TakeDamage(100)
			wait(0.25)
			enemyClone:Destroy()
		end)
		wait(0.5)
	end
end

Then there is issue #2:
This lies with my placement system. I have another video which displays my issue here:


In the video the first tower is placed fine then the second tower is placed way high up off the ground. Now while I did intersect the towers in placement which can cause the issue, the issue has also happened sometimes when placing a tower regularly.
Here is the whole placement system, as I am unsure what part is causing the issues:

--Client
local replicatedStorage = game:GetService("ReplicatedStorage")

local placeEvents = replicatedStorage.Events.PlaceEvents:FindFirstChild("Place")

local equippedTowers = script.Parent.EquippedTowers

local towerFolder = workspace:FindFirstChild("Map").Towers

local player = game.Players.LocalPlayer

local mouse = player:GetMouse()

local posX = nil
local posY = nil
local posZ = nil

local debounce = false

local placing = false

local gridSize = 0.25

local confirmed = false

local function snap()
	posX = math.floor(mouse.Hit.X / gridSize + 0.5) * gridSize
	posY = mouse.Hit.Y
	posZ = math.floor(mouse.Hit.Z / gridSize + 0.5) * gridSize
end

local function movement(tower)
	mouse.TargetFilter = tower
	
	snap()
	
	if tower ~= nil then
		tower:SetPrimaryPartCFrame(CFrame.new(posX, posY, posZ))
	end
	
	local target = mouse.Target
	local config = tower.Configuration
	local base = tower.Tower.Base
	local hitBox = tower.Tower.HitBox
	local min = base.Position - (base.Size / 2)
	local max = base.Position + (base.Size / 2)
	local hitBoxMin = hitBox.Position - (hitBox.Size / 2)
	local hitBoxMax = hitBox.Position + (hitBox.Size / 2)
	local towerRegion = Region3.new(min, max)
	local hitBoxRegion = Region3.new(hitBoxMin, hitBoxMax)
	local intersectingParts = workspace:FindPartsInRegion3(towerRegion, base) --Get any intersecting parts with the tower base
	local touchingHitBox = workspace:FindPartsInRegion3(hitBoxRegion, hitBox) --Get any intersecting parts with the tower hitbox
	local intersectingHitBox = nil
	for _, hitboxPart in pairs(touchingHitBox) do --Loop through the list intersecting the hit box to check for another tower hitbox (Basically if there is an intersecting hitbox you are placing inside another tower)
		if hitboxPart.Name == "HitBox" then
			intersectingHitBox = hitboxPart
		end
	end
	local touchingPath = nil
	for _, pathPart in pairs(intersectingParts) do --Loop through the list intersecting the tower base to check for paths that way you can't place the tower on the path
		if pathPart.Name == "Path" then
			touchingPath = pathPart
		end
	end
	if placing == true and target.Name == config.Placement.Value and intersectingHitBox ~= "HitBox" and touchingPath ~= "Path" and player.GamePlayData.TowersPlaced.Value < player.GamePlayData.MaxTowers.Value and player.GamePlayData.Cash.Value >= config.PlaceCost.Value and player.PlayerData.Wood.Value >= config.CraftingCost.Wood.Value and player.PlayerData.Stone.Value >= config.CraftingCost.Stone.Value and player.PlayerData.Metal.Value >= config.CraftingCost.Metal.Value and player.PlayerData.Crystal.Value >= config.CraftingCost.Crystal.Value and player.PlayerData.Plasma.Value >= config.CraftingCost.Plasma.Value then
		confirmed = true
		tower.Tower.HitBox.Color = Color3.fromRGB(85, 255, 127)
	else
		confirmed = false
		tower.Tower.HitBox.Color = Color3.fromRGB(255, 0, 0)
	end
end

local function startPlacement(tower)
	for i, placedTower in pairs(towerFolder:GetChildren()) do
		placedTower.Tower.HitBox.Transparency = 0
		placedTower.Tower.HitBox.BrickColor = BrickColor.new("Medium stone grey")
	end
	
	tower.Parent = workspace.Map.Towers
	
	tower.Tower.HitBox.Transparency = 0.5
	
	tower.Tower.HitBox.Color = Color3.fromRGB(85, 255, 127)
	
	tower.Tower.Range.Size = Vector3.new(0.25, tower.Configuration.Range.Value, tower.Configuration.Range.Value)
	
	mouse.Move:Connect(function()
		if placing == true then
			movement(tower)
		end
	end)
end

for i, towerSlot in pairs(equippedTowers:GetChildren()) do
	if towerSlot:IsA("ImageButton") then
		towerSlot.MouseButton1Click:Connect(function()
			if placing == false then
				placing = true
				local towerModel = replicatedStorage.Towers:FindFirstChild(player.TowerSlots["1"].Value)["1"]:FindFirstChild(player.TowerSlots["1"].Value)
				local cloneTower = towerModel:Clone()
				startPlacement(cloneTower)
				
				mouse.Button1Down:Connect(function()
					if confirmed == true and debounce == false then
						debounce = true
						placeEvents:FireServer(Vector3.new(posX, posY, posZ), cloneTower.Name, cloneTower.Configuration.PlaceCost.Value, cloneTower.Configuration.CraftingCost.Wood.Value, cloneTower.Configuration.CraftingCost.Stone.Value, cloneTower.Configuration.CraftingCost.Metal.Value, cloneTower.Configuration.CraftingCost.Crystal.Value, cloneTower.Configuration.CraftingCost.Plasma.Value)
						cloneTower:Destroy()
						confirmed = false
						placing = false
						for i, placedTower in pairs(towerFolder:GetChildren()) do
							placedTower.Tower.HitBox.Transparency = 1
						end
						wait(1)
						debounce = false
					else
						print("Can't place")
					end
				end)
			end
		end)
	end
end

--Server
local replicatedStorage = game:GetService("ReplicatedStorage")

local placeEvent = replicatedStorage.Events.PlaceEvents:FindFirstChild("Place")

placeEvent.OnServerEvent:Connect(function(player, placePos, towerName, placeCost, wood, stone, metal, crystal, plasma)
	local placedTowers = player:WaitForChild("GamePlayData").TowersPlaced
	local towerLimit = player:WaitForChild("GamePlayData").MaxTowers
	if placedTowers.Value < towerLimit.Value and player.GamePlayData.Cash.Value >= placeCost and player.PlayerData.Wood.Value >= wood and player.PlayerData.Stone.Value >= stone and player.PlayerData.Metal.Value >= metal and player.PlayerData.Crystal.Value >= crystal and player.PlayerData.Plasma.Value >= plasma then
		local towerToPlace = replicatedStorage.Towers:FindFirstChild(towerName)["1"]:FindFirstChild(towerName)
		local towerClone = towerToPlace:Clone()
		towerClone:SetPrimaryPartCFrame(CFrame.new(placePos))
		towerClone.Configuration.Owner.Value = player.Name
		towerClone.Parent = workspace.Map.Towers
		player.GamePlayData.Cash.Value -= placeCost
		player.PlayerData.Wood.Value -= wood
		player.PlayerData.Stone.Value -= stone
		player.PlayerData.Metal.Value -= metal
		player.PlayerData.Crystal.Value -= crystal
		player.PlayerData.Plasma.Value -= plasma
		player.GamePlayData.TowersPlaced.Value += 1
	end
end)
  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    Issue #1:
    I have not attempted any solutions as I am left scratching my head. It could be my fault for reusing code but it had no issues in a demo I made, and that was how I wanted my enemy spawning to work on for my actual game. So I have no idea why issues are arising when I had no prior issue.

Issue #2:
I am unsure about what to do here mainly because I don’t know what is causing the issue. I think I could go about checking for intersecting Tower Hitboxes better to help reduce any occurrence of the issue but otherwise I am unsure what to do.

I am deeply sorry to make such a lengthy post, these issues have put development on hold for me as I don’t want to progress with development until everything works properly.
If you have any questions regarding the code or other stuff ask away as it can likely help narrow a solution.

1 Like

Ok for the first issue, I highly believe it’s a humanoid physics issue. Since you are moving the enemies via CFrames and position, enable platform stand mode as we don’t want the humanoid physics getting in the way.

For the second issue, there is an error in the output where the primary part has gone missing, presumably for the startPlacement function have you checked that out?

1 Like

I definitely thought the spasm part had to do with humanoid physics but totally forgot about platform stand. However, the enemies don’t end up on the position of the part and end getting set to a position around the position of the part. While that isn’t breaking the game in anyway nor causing problems it isn’t what should be happening
Edit 2: It appears the issue still occurs even after enabling Platform Stand in the humanoid. Not exactly sure why.

The error in the output from what I have seen lies with mouse.Move. When I place the tower I delete the clone that is set to the mouse position so when mouse.Move calls my movement function that error is returned. I forgot to put a check before the movement function is called to ensure the tower and primary part is not nil so I will do so now

Edit: After placing an if statement before the movement function in the mouse.Move function, I have discovered that was causing my problem there. That was a mistake on my end as I totally forgot about that check. Thanks for pointing it out as I forgot that was missing some how