Help with making decision about optimization

Hello,

I am thinking about making some zombie game where you kill zombies.
But, I don’t know should I account for zombie’s optimization or no at early development stages.
I am kinda confused about the optimizations as well… I’ve heard that Humanoids are not optimized, then I found out that you can use ControllerManager… Everything is kinda confusing.

Can anyone help me out with my decision to whatever I should optimize my zombies at early stages or nah, and If yes, how could I do the optimizations?

3 Likes

heres themaximum u can do

server holds only zombie data, no models, if u need hitbox, u can render only ONE big part on server for the purpose of detecting raycasts and etc, instead of many parts and etc
client renders zombie based on data (zombie model etc)
no humanoids, interpolate cframe on client

what i suggest making first is

whenever server spawns a zombie, its model should look something like this

ZombieBoss
     Hitbox (Part)

the model name itself tells you what kind of model to actually render on client, so when client detects a new zombie, it copies model parts into the model, completing the look of the zombie
(no humanoid either btw)

for pathfinding:

don’t compute paths often
first you should check if direction zombie is currently walking to is blocked (raycast 3-5 times a second to detect obstacle)

if theres obstacle, recompute the path again
if theres no obstacle between player and zombie, mimic :MoveTo and just move it to the player

1 Like

Oh, but I don’t really understand the meaning of zombie data.

Should it be updated? Like, If zombie is chasing player, should I update the zombie position in the table (like ZombieData.Position which is Vector3) on the server constantly with Loop/Heartbeat connections?
How would you get the position on the client then? Use remote events? Some other method? Wouldn’t calling too much remote events (even if they just include position data) way too resource-heavy?

Thanks for the answer by the way.

no, server default replication happens at 20Hz (20 times a second), but you can interpolate that on client to have smoother walking.

DOD stands for Data Oriented Design.

it looks somthing like this

local EntityCount = 0
local Entities = {} --// index is EID (entity id, incremented EntityCount)


local Zombies = {} -- EID: boolean, just a quick lookup to see if entity is a zombie
local Position = {} -- eid: vector, this data is for every renderable entity

local function CreateEntity(): number
    EntityCount += 1
    return EntityCount -- our entity is a number
end

local function CreateZombie(): number
    local ZombieID = CreateEntity()
    Position[ZombieID] = vector.zero

    local Model = GetZombieModel()
    Model:PivotTo(Position[ZombieID])
    return ZombieId
end

the flow is like this

local RunService = game:GetService("RunService")

local ZOMBIE_STEP = 1 / 20
local NAVIGATION_TICK_INTERVAL = 4 -- 20 / 4 = 5 Hz

local accumulator = 0
local tickNumber = 0

RunService.Heartbeat:Connect(function(dt)
    accumulator += math.min(dt, 0.25)

    while accumulator >= ZOMBIE_STEP do
        accumulator -= ZOMBIE_STEP
        tickNumber += 1

        for index, zombie in Zombies do
            -- Stagger checks so every zombie doesn't raycast simultaneously.
            local shouldCheckNavigation =
                (tickNumber + index) % NAVIGATION_TICK_INTERVAL == 0

            if shouldCheckNavigation then
                updateTargetAndNavigation(zombie) -- casts a ray to where zombie is facing, updates zombie navigation mode like "Move" (no obstacle, :MoveTo) or "Path" (means compute new path, and update it)
            end

            simulateZombieMovement(zombie, ZOMBIE_STEP) -- checks if navigation mode is "Move" then simply move, if its "Path", then use updated path data and interpolate to it
        end
    end
end)

About the “server default replication” - are you meaning if I use the model example with the Model & Hitbox inside?
Also, about moving - how do I make one?
I tried making the custom rig system driven on data while I was trying to make Tower Defense just for a test, and I did this to calculate current position from one waypoint to second one:

local StartTime = workspace:GetServerTimeNow() -- something like that, but it was 
--setup when the path started, not when the position got calculated
local Speed = 16 -- just a speed number of NPC
local Distance = (SecondWaypoint.Position - OneWaypoint.Position).Magnitude
local TotalDuration = Distance / Speed
	
local ElapsedTime = workspace:GetServerTimeNow() - StartTime -- get time that was passed
-- because the function was ran until calculated position wasn't close enough to the second waypoint
local Progress = math.min(ElapsedTime / TotalDuration, 1)
	
local CurrentPosition = StartPosition:Lerp(TargetPosition, Progress)
return CurrentPosition

Would the example above work?

every change on server replicates at 20hz, any property you change of anything (you move a model, change part color, destroy it, etc) the server will collect all of these changes in a batch, and replicate at 20 times a second.

dont forget to hold angles (where zombie is actually looking)
but other than that it seems correct, is there an issue ur saying?

Oh, so just use .CFrame?

The code that I provided has no issue, I tested it with proper setup and It worked and calculated the position good. (and I also ran it with repeat ... task.wait(0.1) until variable == true), so like only 10 calls per second?)

yes, but u can store pos and angles seperately

thats 10 calls per second if ur fps is 100, server rate is bound to 60hz (.Heartbeat runs only 60 times a second)

How much optimisation you’d want depends how complex your zombies are and how many you want in your game at once.

If you’re making non-smart zombies that just follow the nearest player (like those in the Toolbox), you can get away with doing simple, direct code. Whereas if you want roaming, walking + running, pathfinding + raycasts, and all of that jazz then it would do you good to look into proper game development concepts (see video and related).

As for Roblox-specific optimisations (like the humanoid stuff, as you said), I’d recommend looking for/making a new post specifically for zombie AI and see what others have done. Looking on YouTube will also give you a better idea.

Personally, I just use OOP + state machines. It’s on the easier side to learn and gets the job done. Though I’d encourage you to explore ECS and get into the habit of using that instead, since it proves to be more performant.

As for whether to optimise your game in the early stages, I’d say it depends on your priorities and skill level. If you don’t want to fall into the over-engineering trap and actually get a game out, prioritise optimisation less - you can iterate on it later and each time you redo the system, you’ll naturally learn more and implement more optimisations to your code in general.

Update.
I tried making the optimization. Here’s what I achieved:

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

-- Modules
local Network = require(ReplicatedStorage.Shared.Network)

-- Init
local Step = 1 / 20
local Timer = 0

-- Data
local Components = {
	Count = 0,
	Position = { X = {}, Y = {}, Z = {} },
	Health = {},
	State = {},
	Target = {},
	Type = {},
}
local SpawnModule = {}

-- Local Functions
local function GetPosition(Position1, Position2)
	local Speed = 30

	local direction = (Position2 - Position1).Unit
	local newPos = Position1 + direction * Speed * (1 / 20)
	return newPos
end

local function GetPlayerInRaidus(Position, MinStuds: number): Model?
	for _, Player in Players:GetPlayers() do
		local Character = Player.Character
		if not Character then
			continue
		end

		local HRP = Character:FindFirstChild("HumanoidRootPart")
		if not HRP then
			continue
		end

		local Distance = (Position - HRP.Position).Magnitude
		if Distance <= MinStuds then
			return Character
		end
	end

	return nil
end

local function Clear(id: number)
	Components.Position.X[id] = nil
	Components.Position.Y[id] = nil
	Components.Position.Z[id] = nil
	Components.Health[id] = nil
	Components.State[id] = nil
	Components.Target[id] = nil
	Components.Type[id] = nil
end

-- Functions
function SpawnModule.Spawn(Type: string, Position: {})
	local PosX, PosY, PosZ = Position.X, Position.Y, Position.PosZ

	Components.Count += 1

	Components.Position.X[Components.Count] = PosX
	Components.Position.Y[Components.Count] = PosY
	Components.Position.Z[Components.Count] = PosZ
	Components.Health[Components.Count] = 100
	Components.State[Components.Count] = 1
	Components.Target[Components.Count] = nil
	Components.Type[Components.Count] = Type

	Network:FireAllClients("spawnEnemy", Components.Count, Type, PosX, PosY, PosZ, 100, 1)

	return Components.Count
end

function SpawnModule.UpdateEnemies(dt)
	Timer += dt
	if Timer < Step then
		return
	end
	Timer = 0

	local FinalData = {}

	for i = 1, Components.Count do
		local PosX, PosY, PosZ = Components.Position.X[i], Components.Position.Y[i], Components.Position.Z[i]
		local ComputedPos = Vector3.new(PosX, PosY, PosZ)
		local Target = Components.Target[i]
		local Health = Components.Health[i]

		if Health <= 0 then
			Clear(i)
			continue
		end

		local Character = GetPlayerInRaidus(ComputedPos, 50)
		if not Character then
			Components.State[i] = 1
			Components.Target[i] = nil
			continue
		end

		Components.State[i] = 2

		if not Target then
			Components.Target[i] = Character
		else
			local HRP = Character:FindFirstChild("HumanoidRootPart")
			if HRP then
				local currentPos = GetPosition(ComputedPos, HRP.Position)
				if currentPos then
					Components.Position.X[i] = currentPos.X
					Components.Position.Y[i] = currentPos.Y
					Components.Position.Z[i] = currentPos.Z
					ComputedPos = Vector3.new(currentPos.X, currentPos.Y, currentPos.Z)
				end
			end
		end

		FinalData[i] = {}
		FinalData[i].Position = ComputedPos
		FinalData[i].State = Components.State[i]
		FinalData[i].Target = Components.Target[i]
	end

	Network:FireAllClients("syncEnemy", FinalData)
end

-- Return
return SpawnModule

Also, I did some tests, but I can’t tell if they are good or no:
10 NPC at a time (maximum Recv. = 16 KB/s)
изображение
100 NPC at a time (Maximum Recv. = 80 KB/s)
изображение


500 NPC at a time (Maximum Recv. 700 KB/s)

FPS was around the same at every test, because I used Parts to render current position of enemies instead of custom Rigs.

  1. dont store x, y, z components seperately of Position component, it’s redundant and useless.
  2. you don’t have to to notify all clients to spawn the enemy, your client has to manually listen to something like workspace.Zombies.ChildAdded and then behave on that new zombie.

your DOD system should also have it’s own replication layer, it should batch all component updates, and send out to all clients, and client syncs their DOD table with the server data.

don’t iterate through every component ID, that’s horrible, I gave you example of having Zombie Component as a tag, and iterate through zombies only, you also should check if every component you depend on actually exists

if Health <= 0 then

you should guard first that the health component exists before comparing it with a number.

stuff like this is horrible, use vectors directly

I see, thanks for the corrections.

Does this mean I could use something like Signal, and fire that signal when I do any change with the component?

So, I should:

  • Make the script create a Model with it’s name referencing to the Type value
  • Create Hitbox part inside the Model
  • Edit position of Hitbox part
  • Also maybe make client do some of the calculations (since Target is almost always stays the same and it’s easy to predict where the Enemy is going to be)

Sorry If I this is stupid questions or If I misunderstood you.

Here’s some advice from someone who wasted spent years building prototypes with way too much of a focus on optimization and project structure from the beginning.

Don’t do it.

None of these prototypes of mine got anywhere because they took way too long to make and interest faded as a result.
It’s important to strike while the iron is hot, so your priority should be to get your idea out immediately on paper or into a “working” prototype.

How?

Disregard everything they tell you is wrong to do.
Just make your prototype playable in the way you initially imagined it.
Evaluate it and come up with improvements and ideas, then implement those as well.
It’s okay for it to be a mess under the hood as long as it’s “working” – a proof of concept. That’s a prototype.

You’ll hit many points where certain systems don’t hold up the way they did before; that’s where you modify the implementation by improving on what’s there.

Don’t make the same mistake I did over and over and over and over and over–

1 Like

you do realize prototypes must STILL have a priority, and most of the times its optimization itself

when I was writing a 2D game, I first wrote a prototype that has chunk culling of my 2D world and other micro optimizations, to see how far I can push it in performance, cuz performance was the number one priority for this type of game. if i hadn’t managed to make it optimized, I wouldnt have even continued working on that project cause I knew other people would not be satisfied of the performance

horrible advice from your end

2 Likes

PM me on here Ill send u my simple DOD manager I wrote (has batched replication layer)

actually no, don’t do what I suggested instead do something like this:

your server calls SpawnZombie("ZombieTypeA"), the function handles DOD part:

  1. Creates EntityID
  2. Tags the Entity as a Zombie
  3. Sets up other components such as Position, ZombieType
  4. And the DOD Networking batches all these component changes on an entity and replicates.
  5. The client now has synced DOD tables and entities.
  6. Now the systems, you need a system like RenderZombies on client that runs every 1/30 second, and it iterates through every Zombie, checks if Zombie has no component Model, and sets everything up, and then finally sets the Model component. (Model component is for client mainly, to know that the Entity has been visually prepared)

Example of the system:


local function System()
    for ZombieID in next, Components.Zombie do
        if Components.Model[ZombieID] then  
            continue -- means the client already prepared the client model and marked it 
        end
        
        -- setup everything
        local ZombieType = Components.ZombieType
        
        if ZombieType then
            local Model = ReplicatedStorage.Models.Zombie[ZombieType]:Clone() -- cache the reference instead of doing lookups
            -- local Model = CachedModels[ZombieType] 
            
            -- clone Model parts (legs, arms, clothes, etc) into the actual Zombie model that server created, you can probably use something like
            -- workspace.Zombies:FindFirstChild(ZombieID) to reference it, or a little bloat and have another component `Instance` which marks the server model, but then 
            -- its the same reference to the same model
            
            -- dont forget server only has Hitbox part, nothing else, so you can create Component called `Hitbox` and assign it, server will use it for bullet raycasts and etc.
            
            -- dont forget to cache the models this function already cloned from
        end
    end
end

RunSystem(Heartbeat.Connect, System, 1 / 30) -- connects to Heartbeat and calls the function at 1 / 30 rate

and most of the times its optimization itself

…No, it’s not?
To be fair, it’s entirely subjective to what your goal with the prototype is.
However, many studios make prototypes to see if an idea on paper can work in practice before it’s presented and evaluated.
This is the most popular reason game prototypes are made.

You can absolutely choose to make your prototype with the goal of testing one or more technical implementations, but in light of OP’s question about whether it’s important for their prototype, I explained that this aspect is not necessary if you’re still uncertain and trying to get your idea off the ground. In this case, I believe the most important thing is to get your idea moving.

Optimizations and refactors can come later when you’ve got something working that you’re happy with iterating on.

Optimize only to make the testing experience better if things get out of control. Don’t focus on anti-cheats, optimizations, bug fixing, etc during early stages. Focus on developing, adding features, defining the game mechanics themselves, refactoring said mechanics until you like what you have.

Only focus on optimizing after you are certain that you like the current state of your game, consider it final, and you don’t plan to change it in any major ways afterwards.

Doing that will save you a ton of time you would have wasted on optimizing, changing things, re-optimizing because you changed the underlying mechanism, repeat.

1 Like

Short answer: No. Don’t fall into the premature optimization trap of trying to guess all of the “optimizations” you’ll need for the game to work. This inevitably results in lots of wasted time and systems that are more complex than they need to be, and sometimes buggier or worse-performing than the simplest approach.

The normal, sane approach to development is to code up the simplest possible implementation first, as this is the quickest way to get something working, and the least likely to have bugs. If the simple approach has performance problems, then you identify where the problems are (through profiling and benchmarking) and reason about how to make optimzations that address the performance bottlenecks without changing the end results. During this process, you will ideally use benchmark times so that you can see quantitatively if your code changes are actually improving performance enough to justify whatever complexity they add.