Npc OOP structure/design

Thanks to a lot of guidance from sz_s (https://www.roblox.com/users/451546711/profile) I’m trying to create somewhat of a bootstrap setup for Npc logic.

I’m struggling to how to fit the pieces together now.

*BaseNpc, Guard and Zombie are all Module Scripts
I have a BaseNpc class and two derived classes (Guard and Zombie).
BaseNpc has base function:CheckForNearbyPlayers

The base properties are Position, Health, Speed).
Guards has a non-derived method Patrol and Zombie:Wander

I have Models: Guard and Zombie that both have a local script with a Game Loop/Repeat to look for targets and take action.

I’m not sure how to put this together to create/init the NPCs at runtime and place them on the workspace and have them leverage the Module Scripts.

I feel like I’m both close, but also maybe on the wrong track.

thanks for any help.

3 Likes

Can you copy and paste the scripts so I can easily see what it is? I can’t see what the code is or see what is in the explorer. It would be easier for me to help if I could understand and so I can modify the code for you.

1 Like

Let me know if this helps.
Thanks.

--Game Init

local Guard = require(game.ServerScriptService.Npc.Guard)
local GuardModel = game.ServerStorage:WaitForChild("Guard")

local Zombie = require(game.ServerScriptService.Npc.Zombie)
local ZombieModel = game.ServerStorage:WaitForChild("Zombie")

function main()

	--Create Guard Model
	local guardModel1 = GuardModel:Clone()	
	local rootPart = guardModel1:WaitForChild("HumanoidRootPart")
	rootPart.CFrame = CFrame.new(-1, 0.5, -57)
	--Add guardModel1 to workspace
	guardModel1.Parent = workspace
	
	----Create Guard Module
	local guard1 = Guard.new("-1, 0.5, -57" , 100, 2, 3, 5, "{'-1, 0.5, -57','-1, 0.5, -57'}")
	
	
	--Create Zombie Model
	local zombieModel1 = ZombieModel:Clone()	
	local rootPart = zombieModel1:WaitForChild("HumanoidRootPart")
	rootPart.CFrame = CFrame.new(-25, 0.5, -57)
	--Add zombieModel1 to workspace
	zombieModel1.Parent = workspace
	
	----Create Guard Module
	local zombie1 = Zombie.new("-1, 0.5, -57" , 100, 2, 3, 5, "{'-1, 0.5, -57','-1, 0.5, -57'}")
		
end

main()


--Npc.BaseNpc

--Services
local PlayerService = game:GetService("Players")

BaseNpc = {}
BaseNpc.__index = BaseNpc

function BaseNpc.new(position, health, speed, visionDistance, hearingDistance)
    local item = {}
    setmetatable(item, BaseNpc)

	item.Position = position
	item.Health = health
	item.Speed = speed
	item.VisionDistance = visionDistance
	item.HearingDistance = hearingDistance

    return item
end

function BaseNpc:CheckForNearbyPlayers(npc)
	local npcRootPart = npc:WaitForChild("HumanoidRootPart")
	
	--TODO: Find player within vision distance and line of site
	for _,player in pairs(game.Players:GetPlayers()) do			
		return player
	end
	 
	return nil
end

return BaseNpc

--Npc.Guard

BaseNpc = require(game.ServerScriptService.Npc.BaseNpc)

Guard = {}
Guard.__index = Guard
setmetatable(Guard, BaseNpc)

function Guard.new(position, health, speed, visionDistance, hearingDistance, patrolVectors)
	local item = BaseNpc.new(position, health, speed, visionDistance, hearingDistance)
    setmetatable(item, Guard)

    item.PatrolVectors = patrolVectors

    return item
end

function Guard:Patrol()
	print("Guard Patrol: "..self.PatrolVectors)
end

return Guard

--NpcZombie

BaseNpc = require(game.ServerScriptService.Npc.BaseNpc)

Zombie = {}
Zombie.__index = Zombie
setmetatable(Zombie, BaseNpc)

function Zombie.new(position, health, speed, visionDistance, hearingDistance)
	
	local item = BaseNpc.new(position, health, speed, visionDistance, hearingDistance)
    setmetatable(item, Zombie)

    --item.Powerup = powerup

    return item
end

function Zombie:Wander()
	print("Zombie Wander")
end

return Zombie


--ServerStorage.Guard.Script

local guard = require(game.ServerScriptService.Npc.Guard)

--Declaring variables regarding this NPC/Model
local character = script.Parent
local humanoid = character.Humanoid
local rootPart = character.HumanoidRootPart

--[[
Main function of the NPC, commands the entire script.
It is to be inserted here any new functionalities that this NPC might get,
Such as smart pathfinding, attacking, etc..
For now, it just keeps telling the NPC to move.
]]--

local function main()
	
	humanoid.WalkSpeed = 2 --Property on BaseNpc.Speed
	local target = nil --Player target that is being chased
	local previousTarget = nil --Player target that is being chased
	local targetLastKnownPosition

	repeat
		wait()
	
		--Look for Nearby Players until a Target is returned
		--This logic is in the BaseNpc module
		
		--Take some action if a target is found
		
		--Guard Class Function: Patrol if not found
		
	until humanoid.Health < 1 --loop until dead			
	
end

main()

Looks great! I have a few suggestions but they may be more of preference rather than rule.

I’m noticing you place a Server Script inside the Guard model which controls it, this is perfectly valid but I usually separate the management of assets (the Guard body parts, rig, humanoid, etc.) and code (the controller script). This might be a matter of preference, but I find it easier to manage larger projects when all the code is in one place and all assets are in one place (makes it much easier to use Rojo, which is an excellent plugin for version control).

In your class constructors, you name the new instance variable item, I usually name it self to remind myself I am dealing with an instance of the class in the constructor as well (and to make the code more similar to other OOP languages like Java or Python). Also, I don’t know if you knew this but you can use ... to pass an arbitrary number of arguments. So the first two lines of your Zombie constructor can be equivalently written as

function Zombie.new(...)
	
	local item = BaseNpc.new(...)

That’s sort of all my opinions for now, everything looks great and I will revisit this later today but I need to go for now :smile:

1 Like

Where would you put the Guard Model controller script ?
Inside of ServerStorage/ModelControllerScripts ?

My biggest blind spot is still how to wire the Model I created in the GameInit class with the NpcGuard module and I guess.

thanks for the feedback.

The model I follow is using a single Server Script (and a single LocalScript) for the entire game, which initializes all modules/services and runs them. How I handle mobs is there is a module called MobService which would handle the instantiation of Guard instances and controls/manages the instances. I think it makes sense to put the controller script in the directory you specified.
Another issue to consider is if you put the script inside the Guard model, then the script will be duplicated for every Guard instance, which doesn’t feel right to me.

Moving the Guard script out of the model makes perfect sense and I guess I connect that to the Guard model when I create the mode in my GameInit script.

But I’m still missing how I connect the dots from initializing a guard object (GameInit) that leverages the Npc.Guard module.

  1. GameInit script
    a. New instance of the Guard Model
    b. Connect new Guard model to Guard ControllerScript (moved out of Guard model to ServerScriptsSerivce/ ControllerScript/GuardScript
    I. This will have the Guard loop for calling out to the Npc.Guard script checking for targets and patrolling
    c. How do to leverage the Npc.Guard module code ? This is my new OO class where Guard logic will be. Patrol, Detect players.

Sorry still confused, but feel like I’m close.
thanks.

1 Like

Not entirely sure what you mean, it looks like you will be using Npc.Guard class in your Guard controller? Are you confused about how to instance the class?

1 Like

Let me test what you have suggested first.
Maybe I’m over thinking this

2 Likes

Did you figure this out ? Because I’m at the same stage in my thinking now. How to best actually call the class functions for each individual unit

1 Like