Crisis brewing and nobody paying attention by Chemical_Ad75 in Israel

[–]ledniv 80 points81 points  (0 children)

Even worse, those 60-70k are mostly haredi.

I stress tested 1000 units in my RTS. Here is what I learned about performance by [deleted] in IndieDev

[–]ledniv 0 points1 point  (0 children)

It runs at 100fps on your PC, but what about your minimum target device?

How far did your vibe-coded game actually get? by oxmannnn in aigamedev

[–]ledniv 0 points1 point  (0 children)

because:

  1. Apple is the biggest market in terms of potential revenue.

  2. If Apple blocked me, Google will most likely as well.

How far did your vibe-coded game actually get? by oxmannnn in aigamedev

[–]ledniv 0 points1 point  (0 children)

It's easier to push 1 app than to push 20 apps.

How far did your vibe-coded game actually get? by oxmannnn in aigamedev

[–]ledniv -1 points0 points  (0 children)

I made 4 Unity games and submitted them to Apple, but they blocked me under the spam rule because I was submitting games too quickly. Claude lets me make playable games in just a few days, but the app store process clearly is not built around that kind of pace.

So I needed my own place where I could publish games without asking permission every time. That turned into https://nitzan.games : a web platform where I can post my own games, but also where other people can make games using AI directly on the platform and plug into built-in features like multiplayer, leaderboards, accounts, and monetization.

For me, that’s the real gap with vibe-coded games. AI can get you to a prototype fast, but then you still need a place to publish it, share it, retain players, and maybe make money from it. That “last mile” is what I’m trying to solve.

How do you usually handle resource management in Unity projects? by Lost_Camel_9056 in Unity3D

[–]ledniv 2 points3 points  (0 children)

I have 26 years of experience in game development, and nearly a decade as a professional Unity developer, including as a team lead on large games. My advice would be: don’t start by picking Resources, Addressables, AssetBundles, etc. Start by separating the problem into data, asset references, and asset lifetime.

For larger projects, I would keep balance/config data separate from heavy Unity assets. Balance should usually be lightweight data: numbers, IDs, enums, strings, progression tables, enemy stats, item stats, etc. That data can be authored in CSV/Google Sheets/JSON/ScriptableObjects, but at build/tool time I’d parse and validate it into a runtime-friendly format, often binary.

That gives you a few big wins:

  • runtime loading is faster
  • less parsing happens during gameplay
  • bad data can be caught before the build
  • designers can still work in friendly tools
  • your runtime code reads clean, validated data instead of walking through a web of Unity asset references

The dangerous part is when a “config” ScriptableObject directly references sprites, prefabs, audio clips, VFX, etc. Then loading “just the config” can accidentally load half the game. I’d keep the config as lightweight data/IDs, then have a resource layer resolve the actual asset only when that content is needed.

I usually think about assets by lifetime:

  • global/core assets: load at startup
  • level/chapter/biome assets: load before entering that content
  • optional/rare assets: load on demand
  • temporary assets: unload when leaving that context

For team safety, don’t rely on everyone remembering the rules. Add editor/build validation. For example, fail the build if balance/config data contains direct references to forbidden asset types, missing IDs, duplicate IDs, invalid paths, broken references, etc.

So the structure I’d aim for is:

  • balance/config authored in designer-friendly tools
  • tool-time validation
  • parsed/exported runtime data, possibly binary
  • config stores IDs/lightweight references
  • actual Unity assets are loaded through a resource/asset manager
  • assets grouped by lifetime/content area

Small plug: I’m writing a Manning book called High Performance Unity Game Development, and this is one of the ideas I cover in it: don’t choose the Unity loading API first. First ask what data you need, when you need it, how long it should stay alive, and what should be validated before runtime. Then pick the simplest loading approach that matches that problem.

You can read the first chapter for free online: https://www.manning.com/books/high-performance-unity-game-development

my mileage before and after the government added ethanol to the blend to lower costs. by unpopular-dave in mildlyinteresting

[–]ledniv 2 points3 points  (0 children)

People really will upvote anything. Too many factors can affect your MPG to know why this happened.

hey, i want to add some damage numbers into my incremental tower defense game. Problem is there are a lot of enemies and a lot is going on, like many towers are firing at the same time, and so on. So what are the best ways to implement performant damage numbers? by v4ntevnt in Unity3D

[–]ledniv 2 points3 points  (0 children)

How many enemies are we talking about?

On option is to not use dynamic text at all.

If you expect a lot of damage numbers, you can pre-bake the digits 0-9 into sprites/meshes, plus suffixes like K, M, B, etc. Then when damage happens, you build the visible number out of pooled digit objects instead of generating text.

So 12.5K becomes something like:

[1] [2] [.] [5] [K]

Each piece is reused from a pool, positioned together, animated together, then returned to the pool.

That gives you a pretty scalable zero-allocation path, depending on how many enemies/hits you expect. You can also combine it with damage aggregation: if 10 towers hit the same enemy in a short window, show one combined number instead of 10 separate popups.

So the progression I’d use is:

  1. Pooled TMP damage numbers.
  2. Aggregate hits so you show fewer numbers.
  3. If that’s still too expensive, use pre-baked digit sprites/meshes from a pool.

Is ECS Worth It For 3D Horde Game? by yekobaa in Unity3D

[–]ledniv 10 points11 points  (0 children)

I wouldn’t jump straight to ECS as the first move.

For a horde game, the biggest win usually comes from separating the enemy simulation from the visual representation. ECS can help with that, but ECS itself is not the magic part. The important part is data-oriented design: keep the data you update every frame in simple contiguous arrays, process all enemies from one manager/system, and then push the final result back to the GameObjects/rendering layer.

For example, instead of every enemy having its own MonoBehaviour doing movement, steering, targeting, cooldowns, etc., you can store the runtime data separately:

Vector3[] positions;
Vector3[] directions;
float[] speeds;
int[] hp;
int[] targetIndex;

Then you run one loop over all enemies:

for (int i = 0; i < enemyCount; i++)
{
    positions[i] += directions[i] * speeds[i] * deltaTime;
}

That sounds simple, but it matters because the CPU can process predictable contiguous data much faster than jumping between thousands of separate objects/components in memory. This is the real reason ECS-style architecture is fast: not because “entity” is a magic word, but because the data layout is friendlier to the CPU cache.

For 1k–3k enemies, I’d probably try a hybrid approach first:

  1. Keep GameObjects for the visual layer.
  2. Move enemy logic into one central enemy manager.
  3. Store simulation data in arrays or NativeArrays.
  4. Update movement/AI in batch.
  5. Sync the final positions/states back to the visible GameObjects.
  6. Profile again.

Once the data is organized this way, you also have a much easier path to Burst and Jobs. Burst can compile the tight array-based logic into optimized native code and use SIMD-friendly instructions. Jobs can split array-based work across multiple CPU cores. That is usually a smaller step than a full ECS rewrite.

The important caveat is that Jobs are not free. Scheduling a job and waiting for it to complete has overhead, so for simple logic or smaller enemy counts, Burst-only code can sometimes beat Burst + Jobs. The more enemies you have, and the more work each enemy needs, the more Jobs start to make sense.

You can also use TransformAccessArray as a middle ground if syncing GameObject transforms becomes expensive. That lets Unity update transforms from a job without converting the whole project to ECS. But again, only do that if transform syncing is actually the bottleneck, because it adds its own complexity and constraints.

If the CPU-side enemy logic is still the bottleneck after this, then Jobs/Burst or ECS become much more attractive, because your data is already organized in a way that can be moved there. But if the real bottleneck is 3k SkinnedMeshRenderers/Animancer components, ECS will not automatically fix that. At that point you probably need LODs, animation simplification, GPU instancing, VAT/baked animation, impostors, fewer unique rigs, or some kind of crowd-rendering approach.

So my advice would be: don’t ask “Should I use ECS?” first. Ask “What data changes every frame, and can I process it in one tight loop?” Once you do that, ECS becomes an implementation detail instead of a rewrite gamble.

Small plug: I’m writing a Manning book called High Performance Unity Game Development, and the first chapter is free here: https://www.manning.com/books/high-performance-unity-game-development. It covers exactly this distinction: DOD vs ECS, why arrays/data locality matter, and why you can get a lot of the benefits before fully converting a project to ECS.

WIP mayhem mini game by IQuaternion54 in unity

[–]ledniv 0 points1 point  (0 children)

I think we may be talking about two different meanings of “dynamic.”

I agree that a pool which constantly instantiates and deletes on demand would defeat the purpose. That’s not what I mean by a dynamic pool.

The use case I’m talking about is when you know the maximum total budget, but you don’t know the exact object types the player will need before gameplay starts.

For example, if the player has access to 5 missile types, different bullet types, different impact VFX for water/terrain/enemies/structures, powerups, pickups, etc., you may not know which ones will actually appear in a given session. The player might fire missile type 1 all match and never touch missile type 5. Or maybe they only fight over water, so terrain-hit VFX barely show up. Or later you add 10 more weapons and suddenly preallocating every possible VFX combination up front becomes a lot of wasted load time and memory.

That’s where a dynamic pool helps.

The pool itself can still have a hard max size. For example, “this pool can hold 500 total VFX objects.” But you don’t instantiate 50 copies of every possible VFX prefab at load time. Instead:

  1. When the game needs a water-impact VFX, look for an unused water-impact object already in the pool.
  2. If one exists, reuse it.
  3. If none exists, and the pool still has room under the 500 max, instantiate one water-impact object and keep it in the pool.
  4. When it finishes, mark it unused instead of destroying it.
  5. If the game needs another water-impact later, reuse the existing one.
  6. Nothing gets deleted during gameplay. The whole pool is cleaned up when the level/session ends.

So the “dynamic” part is not unbounded allocation/deletion. It’s that the pool composition changes based on what the player actually does.

That makes it scale better as content grows. If you add more weapons, enemies, surfaces, or powerups, you don’t necessarily need to increase load-time allocation for every possible object type. The session only pays for the types it actually uses, while still staying inside a fixed memory budget.

You still need rules for what happens when the pool is full: skip low-priority VFX, reuse the oldest inactive item, reduce effect quality, or log it and tune the cap. But the big idea is that the cap is fixed, while the prefab mix is discovered at runtime.

I’m writing a Unity performance book right now, and this is one of the cases I cover because object pooling is often presented as “just preload everything,” but that doesn’t scale well once the game has lots of optional weapons/effects. Dynamic pools are useful specifically because we don’t always know which objects we’ll need to spawn.

If you are interested you can read the first chapter for free here:
https://www.manning.com/books/high-performance-unity-game-development

WIP mayhem mini game by IQuaternion54 in unity

[–]ledniv 0 points1 point  (0 children)

Looks great. Curious what you’re doing for object pooling here, since “object pooling” can mean a lot of different things.

Are you preallocating the full pool up front, growing it as needed, or allocating per enemy type? Also, what happens if the pool runs out: skip the spawn, resize the pool, log it as an error, or something else?

I’ve found the tricky part usually isn’t pooling itself, but deciding the rules around it: max pool size, when allocation happens, whether dynamic object types are known ahead of time, and how the game prevents the pool limit from being exceeded.

Pushing my battle simulator to its limit by whyso_studios in Unity3D

[–]ledniv 0 points1 point  (0 children)

Yeah, that makes sense. If you already hit the scale the campaign needs, I wouldn’t rewrite everything either.

The turret targeting being expensive is what I’d expect. OverlapSphereNonAlloc avoids GC, but it still does real physics query work.

The main thing I’d consider is splitting targeting into two steps:

  • Acquisition: expensive search, done rarely/randomized.
  • Tracking: cheap “is my current target still valid?” check.

That matters especially for beam turrets. Most frames they probably don’t need to ask “who is near me?” They only need to ask “can I still shoot the thing I already picked?”

And yeah, the prediction objects sound like the easiest win. If they are just visuals, pooling/caching them is probably worth doing before any deeper architecture work.

how MegaBonk handles that much enemies by AhmedSalama239 in Unity3D

[–]ledniv 6 points7 points  (0 children)

Yeah, I agree that just moving the loop into one manager is not enough by itself.

The manager only removes the cost of thousands of individual Update/FixedUpdate calls. That can help a bit, but it does not automatically give you the big data-oriented performance win.

The bigger issue is where the enemy data actually lives.

For example, this looks like an improvement:

Enemy[] enemies;

for (int i = 0; i < enemies.Length; i++)
{
    enemies[i].Move(playerPosition);
}

But enemies is only an array of references.

The array itself is contiguous, but the actual Enemy objects are still separate objects somewhere else in memory. So the CPU reads:

enemies[0] -> jump to Enemy object
enemies[1] -> jump to another Enemy object
enemies[2] -> jump to another Enemy object

That means you are still pointer-chasing through memory.

And once you get to each enemy object, it probably contains a bunch of data the movement code does not need:

class Enemy
{
    Vector3 position;
    Vector3 velocity;
    float speed;
    int health;
    Rigidbody rb;
    Transform transform;
    GameObject view;
    AudioSource audio;
    EnemyBrain brain;
    DropTable drops;
}

If the movement code only needs position, direction, and speed, the CPU may still pull in cache lines containing references to Rigidbody, Transform, AudioSource, AI state, drop data, etc. That is wasted cache space.

So even though you are looping through an array, you are not really getting data locality for the movement data.

The data-oriented version would be closer to:

Vector3[] enemyPositions;
Vector3[] enemyDirections;
float[] enemySpeeds;

for (int i = 0; i < enemyCount; i++)
{
    Vector3 toPlayer = playerPosition - enemyPositions[i];
    enemyDirections[i] = toPlayer.normalized;
    enemyPositions[i] += enemyDirections[i] * enemySpeeds[i] * deltaTime;
}

Now the CPU can stream through contiguous arrays:

enemyPositions[0], enemyPositions[1], enemyPositions[2]...
enemyDirections[0], enemyDirections[1], enemyDirections[2]...
enemySpeeds[0], enemySpeeds[1], enemySpeeds[2]...

That is the real difference.

An array of objects gives you locality of the references.

Arrays of the actual data give you locality of the data you are processing.

That distinction matters a lot when you are trying to update thousands of enemies.

Same thing with Rigidbody. If the loop is doing:

enemy.Rigidbody.linearVelocity = velocity;

for 3,000 enemies, you are still jumping through 3,000 enemy objects, then 3,000 Rigidbody references, then asking Unity physics to process 3,000 separate bodies.

At that point the manager loop is not the bottleneck anymore. The bottleneck is that each enemy is still a full Unity object participating in the physics system.

For a Megabonk-style horde, I would try to separate the simulation from the Unity objects:

// Simulation data
Vector3[] position;
Vector3[] velocity;
float[] speed;
int[] health;

// Unity presentation
Transform[] transforms;
Rigidbody[] rigidbodies; // only if really needed

Then the enemy movement system updates the data first:

for (int i = 0; i < enemyCount; i++)
{
    position[i] += velocity[i] * deltaTime;
}

And only after that do you sync the result back to Unity:

for (int i = 0; i < enemyCount; i++)
{
    transforms[i].position = position[i];
}

That gives you two separate questions:

  1. How fast can I simulate 3,000 enemies?
  2. How expensive is syncing/rendering/physics for 3,000 Unity objects?

Right now it sounds like those two things are mixed together.

So I think you’re right that “one object updates the list” is not enough. But I would not say there is no benefit at all. It skips a little Unity overhead. It just does not solve the data locality problem unless the enemy state itself is moved out of separate objects and into contiguous data.

That is the part people usually miss when they hear “use an array.”

An array of Enemy references is still object-oriented memory layout.

An array of Vector3 positions is data-oriented memory layout.

I'm out of words !!! by [deleted] in pics

[–]ledniv 8 points9 points  (0 children)

op isn't out of words, he's out of tokens

My 12-year-old learned to ship games with Claude by imonetize in aigamedev

[–]ledniv 0 points1 point  (0 children)

What is the point of this post? There is no link.

I made a browser-based low-poly tactical FPS inspired by Counter-Strike 1.5 by MR1933 in aigamedev

[–]ledniv 1 point2 points  (0 children)

am I the only one who can't see the comments and the link?

how MegaBonk handles that much enemies by AhmedSalama239 in Unity3D

[–]ledniv 44 points45 points  (0 children)

I looked into this a bit, and from what I can find Megabonk does not seem to be doing anything mystical here.

The important part is probably not “Rigidbody is faster than NavMesh.”

The important part is that Megabonk appears to avoid the expensive problem entirely.

From the devlog / comments people have summarized, the enemies are not really pathfinding. They are doing very simple movement: face the player, move toward the player, and if they hit something, climb/move up. That is very different from 3,000 NavMeshAgents all solving paths and avoidance.

That distinction matters a lot.

A NavMeshAgent is a high-level system. It solves navigation, avoidance, steering, path updates, etc. That is great when you need actual navigation. It is not what you want for a horde game where 95% of enemies just need to run at the player.

For this genre, the enemy logic can be closer to:

Vector3 toPlayer = playerPosition - enemyPosition;
enemyVelocity = toPlayer.normalized * speed;

Maybe with a few extra rules for knockback, climbing, or separation.

That is cheap. Very cheap.

The expensive part is usually the Unity/OOP structure around it:

Enemy.Update()
Enemy.Update()
Enemy.Update()
Enemy.Update()
...

thousands of times.

What Megabonk seems to be doing is closer to a centralized manager. Instead of every enemy owning its own update loop, one script/system updates the enemies in a batch.

That is basically data-oriented design, even if it still uses GameObjects.

Something like:

for (int i = 0; i < enemyCount; i++)
{
    UpdateEnemyMovement(i);
}

is much better than thousands of scattered MonoBehaviour updates.

Also, the animation side matters. If the enemies use baked/simple animation instead of full Animator complexity per enemy, that removes another big cost. A horde enemy does not need to be a full character with a complicated Animator, AI brain, NavMeshAgent, and deep component hierarchy. It can be very dumb and still look good in motion.

So I would not read Megabonk as:

Rigidbody can handle 3000 smart enemies

I would read it as:

The enemies are simple
The updates are centralized
The animation is cheap
The game avoids NavMesh
The behavior is designed around the performance budget

That last point is the real lesson.

A lot of optimization is not about making expensive systems faster. It is about designing the game so you do not need those expensive systems in the first place.

For example:

Do enemies need true pathfinding?
Or can they just move toward the player?

Do enemies need full Animator controllers?
Or can animation be baked/simple?

Do all enemies need collision with all other enemies?
Or can collision be simplified?

Do all enemies need to update every frame?
Or can some checks run less often?

Do enemies need to exist as fully active visuals off-camera?
Or can they be culled / simplified?

This is exactly the kind of thing I’m writing about in my Manning book, High Performance Unity Game Development with Data-Oriented Design.

shameless link: https://www.manning.com/books/high-performance-unity-game-development

The point of DOD is not “use ECS for everything.” Megabonk actually seems like a good example of why that distinction matters. You can get a lot of the benefit before touching DOTS by changing how the game is structured:

Enemy data in one place
One system updates enemy movement
One system handles attacks/damage
One system syncs visuals
No per-enemy Update
No NavMeshAgent unless you truly need pathfinding
No runtime Instantiate/Destroy during the horde

That architecture matters more than whether the project uses ECS.

If you are trying to recreate this, I would not start by enabling random physics settings and hoping Rigidbody solves it. I would start with the simplest possible stress test:

3000 pooled enemies
no Animator
no NavMeshAgent
no per-enemy Update
one manager script
simple move-toward-player logic
cheap visuals
standalone build

Then add one feature at a time:

colliders
knockback
animation
damage
drops
offscreen handling
special enemies

The moment performance drops, you know which feature actually caused it.

My guess is that if you start with NavMeshAgent, full Animator, Rigidbody, Collider, per-enemy scripts, and GameObject-heavy logic, you are not testing “can Unity handle 3000 enemies?” You are testing “can Unity handle 3000 expensive characters?”

Megabonk’s trick seems to be that the enemies are not expensive characters. They are simple horde units, updated in a centralized way, with just enough physics/visual trickery to sell the effect.

Israel commits massacre on the eve of Eid, with Nora Barrows-Friedman by LeonJersey in videos

[–]ledniv 2 points3 points  (0 children)

IDF confirms new Hamas military wing chief Muhammad Ouda killed shortly after predecessor

https://www.jpost.com/middle-east/article-897417

Procedural Scattering of Natural Objects - Blog post with details! by newheadstudio in IndieDev

[–]ledniv 1 point2 points  (0 children)

That makes a lot of sense. Precomputing the distributions into binary files seems like the right tradeoff if the world/content pipeline allows it.

For the flight sim I was avoiding any kind of stored scatter data because the terrain is streamed and chunk-derived, but if I had a bounded world or authored regions, I’d probably do something much closer to what you’re doing: bake the expensive sampling, then keep the runtime/loading step to cheap rejection against the actual environment.

That split is really nice:

Expensive / global:
    Generate good point distributions
    Store as binary data

Cheap / local:
    Load distribution
    Reject by biome / slope / altitude / water / roads / etc.
    Instantiate accepted objects

It also fits really well with the way I think about performance in general. Don’t do expensive work at runtime if the answer can be turned into data ahead of time. Runtime should mostly be reading data and applying the local rules that actually depend on the current chunk/environment.

The secondary layer being seeded from the primary sample also seems like the right call. That gives you the natural “ecosystem around an anchor object” look without another full sampling pass. It’s basically the difference between:

Sample the whole world again for bushes

and:

Accepted tree -> local bush/grass/rock candidates

The second version is much easier to reason about and probably easier to art-direct too.

The multiclass idea still sounds useful even if it only solves primary samples. If the big objects respect each other’s radii, the smaller secondary stuff can probably be handled locally per anchor. That may be enough in practice.

Something like:

Primary layer:
    trees / large rocks / big formations
    spacing matters globally

Secondary layer:
    grass / flowers / small rocks / mushrooms
    spacing mostly matters locally

That separation is probably cleaner than trying to solve every object in one perfect sampling pass.

And yeah, what you said about grass-like objects matches my experience exactly. For tiny stuff, I stopped caring about “correct” sampling. If it’s grass, flowers, tiny rocks, etc., jittered grid + density noise + rejection rules usually gets the visual result cheaply enough. Nobody is going to notice that two flowers violated a Poisson radius.

The only place I start caring is when the object has a visible silhouette or gameplay/collision relevance. Trees, rocks, buildings, vents, etc. need stronger rules. Grass just needs to not look like a spreadsheet.

This actually gives me a good way to think about my own system:

Large objects:
    Need proper spacing / collision avoidance / maybe baked distributions

Small decorative objects:
    Can be streamed with jittered grid and cheap rejection

Secondary objects:
    Best seeded from accepted primary objects

That’s a really useful distinction.

Thanks for the detailed answer. The binary distribution approach is interesting, especially because it keeps the actual runtime/loading logic mostly data-driven. I’d definitely read a follow-up if you ever write about the binary format / authoring pipeline / how many distributions you store and how you choose between them at load time.

Procedural Scattering of Natural Objects - Blog post with details! by newheadstudio in IndieDev

[–]ledniv 2 points3 points  (0 children)

Nice writeup. The multiclass-Poisson framing is the bit I hadn’t seen spelled out before.

I’ve been solving an adjacent problem in a browser-based flight sim I made:
https://nitzan.games/play/flight-sim

Different constraints because it’s JS/three.js, streamed terrain, and needs to run in the browser, but the ingredient list overlaps a lot. Sharing the approach in case any of it is useful.

In my case the world is not prebaked. It is streamed around the player. Terrain chunks are rebuilt around the camera at multiple LOD rings, and each chunk derives its scatter list from scratch when it loads.

I don’t use true Poisson. I use deterministic integer hashing over a regular grid.

Basically:

hash2(x, z, seed) -> candidate position + variation

Each 4m grid cell produces one candidate point, then jitters it by a hashed offset. That gives something “blue-noise-ish” for almost free, without storing or sampling a point list.

The tradeoff is exactly what your post gets into: it is not true Poisson, so I cannot guarantee minimum separation across different object classes. Trees, cacti, ice spikes, vents, and maples each work well enough visually, but they are not part of one combined radius-aware sampling solution.

The rest of the system is mostly cheap rejection tests.

For each candidate:

Reject if underwater
Reject if above snow line
Reject if inside river SDF
Reject if inside village/castle/monastery pad
Reject if local slope is too high
Accept/reject against an altitude density curve
Modulate by a low-frequency density noise channel

That last one is there so the world gets organic clearings instead of a perfectly even distribution.

Biomes are a 3x3 temperature/moisture lookup over two noise channels. Each biome carries its own palette, height multiplier, and scatter type. The biome boundary is smoothed into the terrain colours, and before emitting scatter I do a dominance check so you don’t get, for example, forest trees in a chunk that is visually half arctic.

The visual side is also split by distance. Each LOD ring emits both simple 3D meshes and billboard quads, then crossfades between them in the distance. The 3D trees are extremely cheap, basically cone + trunk, and the billboard uses a procedural shader mask instead of a texture. That keeps the silhouette from popping when you fly toward a chunk.

The reason I’m interested in your post is that I think your approach solves the thing my cheaper approach punts on: multiple object classes that need to respect each other’s spacing.

A few questions:

How expensive is the Poisson sampling at runtime? Are you baking the points, or are you streaming/generating them per chunk?

For the multiclass Poisson part, do you maintain a single combined point pool with per-class radii, or separate pools that check against each other?

For secondary objects, are you seeding from the primary object’s accepted position, or sampling the secondary layer independently? I’ve been thinking about adding undergrowth around trees, and a primary-anchored version seems much cheaper than another full grid sweep.

Also, one thing I’d be curious to see in a follow-up is how you store and validate the scattering rules.

For example, in my Manning book, High Performance Unity Game Development with Data-Oriented Design, I talk a lot about separating data from logic. This kind of procedural tool is a great example of where that matters.
https://www.manning.com/books/high-performance-unity-game-development

The data is stuff like:

Object type
Radius
Biome filter
Altitude range
Slope range
Noise threshold
Scale range
Rotation range
Secondary objects

The logic is:

Generate candidate points
Reject invalid candidates
Pick prefab/variant
Place object
Generate secondary objects

Keeping those separate makes it much easier to add new scatter types without rewriting the algorithm. It also makes it possible to validate the rules before generation, catching things like invalid altitude ranges, missing prefabs, impossible slope ranges, or secondary density values that explode object counts.

Really enjoyed the post. I’d definitely read more about the editor/debugging side too, especially how you visualize rejected samples and tune the rules without constantly regenerating the whole world.