Need a second opinion on PVE game

I am making a PVE game with detailed creatures. The creatures are meshes deformed with an armature, and their physical colliders will consist of various parts welded to specific bones in the rig. Currently I am at a crossroads, as there are two ways I can set this system up. Option A, I can create the mesh and the collider on the server and weld them to the root part. Option B, I can create the mesh and the collider on the client and weld them the same way.

Benefits of option A:

  • simple and easy to work with

Cons of option A:

  • potentially expensive as there may be roughly 100-150 of these creatures in the game at any given time.

Benefits of option B:

  • more efficient (in theory) than option A as there will be no mesh rendered on the server and no collider consisting of multiple parts to simulate, just the singular root part.

Cons of option B:

  • many difficulties that will need to be worked around. Server will have to make a rough estimate of where the mouth should be for any time an object may be held in the creature’s mouth. On creature death, the server will have to somehow get the current frame of the animation being played on all the clients, then generate the collider for this creature on the server, then release the ragdoll. Other similar problems may arise later on in development.
1 Like

The server doesn’t render anything.

And for what you should do, I’d say it should be a per-enemy basis.
If the is enemy simple and small, for example, a Wolf, Just have 1 hitbox welded to the root to encapsulate the entire enemy.

If the enemy is large and complicated, for example, a Dragon, then do per-limb hitboxes.

And most importantly, with the amount of enemies you’re going to have, avoid using Humanoids.

A poor choice of words on my part. What i was meaning to say was that the server has to calculate the physics and hitbox of this 3d model. I can set the collision fidelity to box and disable collisions, but im not sure if that fully removes it from the equation in terms of server cost

This will have variable value to the game. I am making a hunting game, where all the creatures will have vital points which grant bonus damage and loot bonuses for kills, so pretty much everything will need somewhat detailed hitboxes. The amount of detail though may be could be reduced for smaller, more common enemies though, maybe just having a single block and then two more blocks for the heart and the head for an enemy like a rabbit. Even then though, i do want to have high fidelity colliders for when the enemy dies and spawns a ragdoll. Maybe i can add more pieces to the collider once the creature dies, and that increased fidelity becomes necessary?

My plan is to use a single, game-wide controller that manages all enemies in the game. Each enemy will have its own object created from a module which contains all of their stats, and then the manager script will interpret this data to handle all of their behaviors and whatnot. Enemy physics will be handled with the new CharacterControllers that were added to the engine a few months ago.

Does this mean that you believe all colliders should be handled on the server?

1 Like

To get a better idea of what this cost would look like running on the server, I ran a quick script to spam this model across the map during runtime:

The model contains 9 parts in total, all welded to the singular root part (the ball). This should roughly be the average number of parts in an actual server-simulated model, which cumulatively should be less expensive together as they are all welded to one part. During the play test, I ran a script that replicates this model 350 times randomly across the baseplate, and throws the model into the air every 1-4 seconds. This is what the stress test on the server (my pc) looks like:

b4955b90c53c58b1174e72d99cf5e798

There are no other costly scripts running on the server, just the one being tested. 350 entities is a tad bit extreme and probably not representative of how the server would ideally look, but I figured it would be best to test the worst case scenario. What do you guys think of these stats, are they too much?

I believe you should really focus on implementing some suitable option of bounding box hitbox solution as it changes whole complexity increase from exponential to linear and IF you would still have issues with performance you could easily lend some animals to some clients (as in setting their network ownership and letting select players calculate some animals).
There are two solutions on lending animals to clients:

  1. Let them calculate physics or other stuff and get it sent to server
    roblox’s server physics run at 1/30 fps and then get interpolated to 60 and that should give you enough time for processing between updates.
  2. Give player networkownership and let them control whole entity
    Much simpler, less hurdles, prolly more consistent but less secure which can still be mitigated be serverside checks

Also if you sometimes need more collisions you could simply just change collision type depending on current state, for example idle: static bbox

Ah and for this amount of entities you probably know about it, but ECS (entity-component-system) is almost must have
contradictory to popular belief simple inheritance is not really that different as functions do not get copied, they just get indexed and used with modified self (which is almost literally ECS but with less steps) and variables get assigned to object. ECS will enable you to have dictionary for different entities, and in the future if you need further optimization you can for example prioritize animals closer to players, cleanup regions where there are no players and essentially do much more besides optimizations you are trying to do.

I also believe that 350 is extremely overkill as you will utmost have 200 hundred if you implement region optimizations, player prioritization or other fun stuff.

Every time i think i finally fully understand this engine, i encounter another piece of black wizardry. Im probably gonna stick to my OOP for now, but im sure ill be forced to learn this eventually lol

oh yeah absolutely, I chose 350 for the test because i wanted to give myself a bit of breathing room, ideally they shouldnt exceed 200 or so.

Honestly, come to think of it, I may be able to get away with just having the mesh on the server and have all the colliders on the client. I really only ever need the colliders to exist on the server once the creature dies and ragdolls. I was planning on having just some loose sanity checks for hitreg anyways, since this game isnt a competitive FPS and that kind of thing doesnt matter as much

1 Like

Haha it is not really that hard to wrap your head around it when you figure it out and you can actually make it work in the same way as OOP if needed!

If I were to give brief explanation it would be essentially:
Instead of inheriting:

function clientRuntime.Get(bindableName)
	_assertString(bindableName)

	local bindableEvent = identifiersFolder:WaitForChild(bindableName)
	
	local _clientRuntimeHolder = {}
	setmetatable(_clientRuntimeHolder, clientRuntimeHolder)
        --// Inheritance of functions or looking up for functions and modifying self

	_clientRuntimeHolder.BindableEvent = bindableEvent
	_clientRuntimeHolder.OnClientEvent = signalHandler.New(
		bindableEvent
	)

	globalRemote["OnClientEvent"]:Connect(function(...)
		_clientRuntimeHolder:_OnclientEvent(...)
	end)
	unreliableGlobalRemote["OnClientEvent"]:Connect(function(...)
		_clientRuntimeHolder:_OnclientEvent(...)
	end)

	return _clientRuntimeHolder
end

You simply use functions from other module to actually function properly!

function networkSchemer.NewReciever(_table, schemeReverse, setterScheme)
	local _networkSchemerHolder = {}
	
	_networkSchemerHolder.TargetTable = _table
	_networkSchemerHolder.SchemeReverse = schemeReverse
	_networkSchemerHolder.SetterScheme = setterScheme

	recieverModule.GenerateChanges(_networkSchemerHolder)
	recieverModule.GenerateSetterSchema(_networkSchemerHolder)
        --// This is essentially ECS! You are using functions from other tables while passing self
        --// So if you use self in said function it refers to _networkSchemerHolder 

	return _networkSchemerHolder
end

I thought using real examples would benefit the explanation!

This seems like a good balance between performance and latency!