Using OOP with NPCs

Hey there, I just have a question about using OOP with my NPC system. I have the skeleton set up, but I ran into an issue while trying to continue.

My setup:
image

My NPCs will all be extensions of the main class and managed by the “MainHandler”.

(main class shown below)

-- Module Table --
local NPC = {}
NPC.__index = NPC


-- NPC Constructor --
function NPC.new()
	
	-- Setup --
	local newNPC = {}
	setmetatable(newNPC, NPC)
	
	-- Properties --
	newNPC.Life = 100
	newNPC.Speed = 16
	newNPC.ViewDistance = 45
	newNPC.AttackDistance = 2
	
	-- Return NPC --
	return newNPC
end


-- Module Table --
return NPC

(walker class shown below)

--(( Modules ))--
local NPC = require(script.Parent)
local Utility = require(script.Parent.UtilityModule)

--(( Character ))--
local WalkerModel = game.ServerStorage.CharacterStorage.Walker


-- Module Table --
Walker = {}
Walker.__index = Walker
setmetatable(Walker, NPC)


--(( Zombie Constructor ))--
function Walker.new(spawnPoint)
	
	--(( Setup ))--
	local newWalker = NPC.new()
	setmetatable(newWalker, Walker)
	
	--(( Character ))--
	newWalker.Character = WalkerModel:Clone()
	newWalker.Humanoid = newWalker.Character.Humanoid
	newWalker.Root = newWalker.Character.HumanoidRootPart
	
	--(( Update Humanoid ))--
	newWalker.Humanoid.WalkSpeed = 10
	
	--(( Spawn ))--
	newWalker.Character.Parent = game.Workspace
	newWalker.Root.CFrame = spawnPoint
	
	--(( Load Animations ))--
	newWalker.Animations = Utility.getAnimations("Walker", newWalker.Humanoid.Animator)
	
	--(( Walking Connections ))--
	newWalker.Humanoid.Running:Connect(function(speed)
		if speed > 0.5 then
			newWalker.Animations["Walk"]:Play()
			newWalker.Animations["Idle"]:Stop()
		elseif speed <= 0 then
			newWalker.Animations["Idle"]:Play()
			newWalker.Animations["Walk"]:Stop()
		end
	end)
	
	--(( Return Zombie ))--
	return newWalker
end


--(( Module Table ))--
return Walker

The problem I face is that I do not know how to run code for an individual NPC; things like detecting nearby Players, chasing a Target, or performing some other action. Should I create a coroutine for each NPC to run these loops? Or is there an alternative method I should use?

2 Likes

When I make NPCs chase targets and perform actions I often use coroutines. I don’t really see any problem with it, either. I guess the only thing I can say is make sure the loop stops when the NPC dies. :man_shrugging:

1 Like

I haven’t used coroutines at all before, so I’m a little unsure of the proper way to implement it. Would I simply make the coroutine in the Main Class and call resume on it from the handler? Or would this be something I should create outside of the classes?

https://developer.roblox.com/en-us/api-reference/lua-docs/coroutine

(Usually I use coroutine.wrap())

Coroutines are certainly useful and will solve this problem. I however, just add all of the npcs to a list and loop through the list calling an :Update() function on all my npcs. It’s a bit trickier to yield since you can’t use wait without delaying the npcs behind it. I like it though because I always have direct control over when exactly each line will run relative to the other npcs.

I’m a little confused, I made the coroutine in the class like I mentioned, so that I could just reference the object to resume it, but it’s acting unexpectedly.

-- (( New Walker )) --
local myWalker1 = Walker.new(CFrame.new(0,20,8) * CFrame.Angles(0,0,0))
--(( Idle Routine ))--
newWalker.Idle = coroutine.create(Utility.Idle(newWalker))
--(( Idle Zombie ))--
function UtilityTable.Idle(Zombie)
	while true do
		print("running!")
	end
end

According to the API article “coroutine.create” only makes the object and it shouldn’t run until it’s resumed, yet the function runs anyways. Have I done something wrong?

Yes. You’re calling the function so it start it and will pass the returned value to coroutine.create. It takes just the variable name. Since you have parenthesis it thinks you want the returned value.

You pass parameters separated by comas in coroutine.resume.

2 Likes

You could add a variable to your NPC class that has a reference to the model.

Spawn NPC, create a bindable event inside that NPC with a function attached to it like so:

BindableEvent.Fired(Connect(function()
 local myNPC = module.NPC.new(ARGS)
 myNPC.model = BindableEvent.Parent
 
 while BindableEvent.Parent ~= nil do
  myNPC.DoLogic()
  --May wanna put something like a wait() here or inside the logic function?
 end
end)

You can keep running logic inside the NPC class until it’s parented to nil.
Destroying the model (and the BindableEvent inside) should eventually clean up the class and remove it from memory if I’m correct.