[SOLVED] Max clone amount for genetic organisms

Hello, I am simulating simple plant (tree-like) organisms for a universe simulation. I have issues with max clone amount, as currently it doesn’t work, and plants often over populate or just go extinct. I want to make sure that no clones can exceed the max amount derived from the original entity.

Start of simulation test run 1 (1 entity died, added 2 new organisms which inherit it’s dna)

Populations over taking almost the full surface of the planets:

It looks like they go extinct at a point, but really they all die, and their clones grow and repeat the same process. I will share the code, just be warned that it is very very long.

   var Life = pc.createScript('life');

// DNA attribute as a string with nine characters
Life.attributes.add('dna', { type: 'string', default: '114221200', title: 'DNA' });

// Number of genomes to mutate
Life.attributes.add('mutationRate', { type: 'number', default: 1, title: 'Mutation Rate' });

// Max clones attribute
Life.attributes.add('maxClones', { type: 'number', default: 50, title: 'Max Clones' });

// Materials array
Life.attributes.add('materials', { type: 'asset', assetType: 'material', array: true, title: 'Materials' });

// Entities array for the first genome
Life.attributes.add('entities1', { type: 'entity', array: true, title: 'Entities 1' });

// Entities array for the second genome
Life.attributes.add('entities2', { type: 'entity', array: true, title: 'Entities 2' });

// Position entities array for entities1
Life.attributes.add('posEntities1', { type: 'entity', array: true, title: 'Position Entities 1' });

// Position entities array for entities2
Life.attributes.add('posEntities2', { type: 'entity', array: true, title: 'Position Entities 2' });

Life.prototype.initialize = function() {
    this.dnaString = String(this.dna);
    this.applyEnvironmentalEffects();
    this.applyDnaMaterials();
    this.enableAndGrowEntities();
    this.startReproduction();
    this.startLifespanCountdown();
};

Life.prototype.applyEnvironmentalEffects = function() {
    var environmentalEffects = this.getEnvironmentalEffects();
    if (environmentalEffects) {
        this.adjustDnaForEnvironmentalEffects(environmentalEffects);
    }
};

Life.prototype.getEnvironmentalEffects = function() {
    var environmentalScript = this.app.root.findByName('EnvironmentalManager').script.environmentalGenomeEffect;
    var activeSun = environmentalScript.getActiveSun();
    return activeSun ? environmentalScript.sunEffects[activeSun] : null;
};

Life.prototype.adjustDnaForEnvironmentalEffects = function(effects) {
    var environmentalManager = this.app.root.findByName('EnvironmentalManager');
    var environmentalScript = environmentalManager.script.environmentalGenomeEffect;
    var dnaValueForColor = this.dnaString.charAt(6);
    var entityColor = environmentalScript.dnaColorMapping[dnaValueForColor];
    if (entityColor === effects.favoredColor) {
        this.dnaString = this.dnaString.substr(0, 1) +
                         this.increaseDnaValue(this.dnaString.charAt(1), effects.reproductionRateIncrease) +
                         this.dnaString.substr(2, 4) +
                         this.increaseDnaValue(this.dnaString.charAt(6), effects.sizeIncrease) +
                         this.dnaString.substr(7, 1) +
                         this.increaseDnaValue(this.dnaString.charAt(8), effects.extraOffspring);
    }
};

Life.prototype.increaseDnaValue = function(dnaValue, increaseAmount) {
    var newValue = parseInt(dnaValue) + increaseAmount;
    if (newValue > 9) newValue = 9;
    return newValue.toString();
};

// Add your new functions here
Life.prototype.enableEntities = function() {
    this.enableEntitiesByDna(this.entities1, this.dnaString.charAt(9)); // 10th genome for entity1
    this.enableEntitiesByDna(this.entities2, this.dnaString.charAt(10)); // 11th genome for entity2
};

Life.prototype.enableEntitiesByDna = function(entitiesArray, dnaValue) {
    var index = parseInt(dnaValue, 10) - 1;
    entitiesArray.forEach(function(entity, i) {
        entity.enabled = (i === index);
    });
};

Life.prototype.applyDnaMaterials = function() {
    this.applyMaterialByDna(this.entities1, this.dnaString.charAt(0));
    this.applyMaterialByDna(this.entities2, this.dnaString.charAt(6));
};

Life.prototype.applyMaterialByDna = function(entitiesArray, dnaValue) {
    var materialIndex = parseInt(dnaValue, 10) - 1;
    if (materialIndex < 0 || materialIndex >= this.materials.length) {
        console.error('Invalid DNA material index: ', materialIndex + 1);
        return;
    }
    var materialAsset = this.materials[materialIndex];
    if (!materialAsset || !materialAsset.resource) {
        console.error('Material asset not found for DNA: ', dnaValue);
        return;
    }
    entitiesArray.forEach(function(entity) {
        if (entity) {
            var renders = entity.findComponents('render');
            renders.forEach(function(render) {
                render.meshInstances.forEach(function(meshInstance) {
                    meshInstance.material = materialAsset.resource;
                });
            });
        }
    });
};

Life.prototype.enableAndGrowEntities = function() {
    this.enableEntitiesByDna(this.entities1, this.dnaString.charAt(9));
    this.teleportEntityToEnabledPosition(this.entities1, this.posEntities1);
    this.enableEntitiesByDna(this.entities2, this.dnaString.charAt(10));
    this.teleportEntityToEnabledPosition(this.entities2, this.posEntities2);
};

Life.prototype.enableAndGrowEntities = function() {
    // Using the second last character of the DNA string for entities1
    this.enableEntitiesByDna(this.entities1, this.dnaString.charAt(this.dnaString.length - 2));
    this.teleportEntityToEnabledPosition(this.entities1, this.posEntities1);

    // Using the last character of the DNA string for entities2
    this.enableEntitiesByDna(this.entities2, this.dnaString.charAt(this.dnaString.length - 1));
    this.teleportEntityToEnabledPosition(this.entities2, this.posEntities2);
};

Life.prototype.teleportEntityToEnabledPosition = function(entitiesArray, posArray) {
    var enabledPosEntity = posArray.find(function(posEntity) {
        return posEntity && posEntity.enabled;
    });

    if (enabledPosEntity) {
        var enabledPos = enabledPosEntity.getPosition();
        entitiesArray.forEach(function(entity) {
            if (entity) {
                entity.setPosition(enabledPos);
            }
        });
    }
};

Life.prototype.enableEntitiesByDna = function(entitiesArray, dnaValue) {
    var index = parseInt(dnaValue, 10) - 1;
    entitiesArray.forEach(function(entity, i) {
        entity.enabled = (i === index);
    });
};


Life.prototype.applyGrowth = function(entity, growthDna) {
    var growthTargetGenome = parseInt(growthDna, 10);
    var scaleFactor = ((growthTargetGenome - 1) / 8) * 0.0008 + 0.0001;
    this.scaleEntityOverTime(entity, scaleFactor, 5);
};

Life.prototype.scaleEntityOverTime = function(entity, targetScale, duration) {
    var currentWorldScale = entity.getWorldTransform().getScale();
    var initialScale = entity.getLocalScale().clone();
    var targetLocalScale = new pc.Vec3(
        targetScale * initialScale.x / currentWorldScale.x,
        targetScale * initialScale.y / currentWorldScale.y,
        targetScale * initialScale.z / currentWorldScale.z
    );

    var startTime = Date.now();
    var scaleInterval = setInterval(function() {
        var elapsedTime = (Date.now() - startTime) / 1000;
        var progress = elapsedTime / duration;
        if (progress < 1.0) {
            var newScale = new pc.Vec3(
                pc.math.lerp(initialScale.x, targetLocalScale.x, progress),
                pc.math.lerp(initialScale.y, targetLocalScale.y, progress),
                pc.math.lerp(initialScale.z, targetLocalScale.z, progress)
            );
            entity.setLocalScale(newScale);
        } else {
            entity.setLocalScale(targetLocalScale);
            clearInterval(scaleInterval);
        }
    }, 1000 / 60);
};

Life.prototype.startReproduction = function() {
    var reproductionRateGenome = parseInt(this.dnaString.charAt(2), 10);
    var reproductionRate = reproductionRateGenome * 10;
    var reproductionAmountGenome = parseInt(this.dnaString.charAt(3), 10);
    var reproductionAmount = reproductionAmountGenome;
    this.reproductionTimerId = this.setReproductionTimer(reproductionRate, reproductionAmount);
};

Life.prototype.setReproductionTimer = function(rate, amount) {
    var self = this;
    return setInterval(function() {
        if (self.entity && self.entity.parent) {
            for (var i = 0; i < amount; i++) {
                self.reproduceEntity();
            }
        }
    }, rate * 1000);
};

Life.prototype.reproduceEntity = function() {
    var cloneCount = this.entity.parent.findByName(this.entity.name).length;
    if (!this.entity || !this.entity.parent || cloneCount >= this.maxClones) {
        return;
    }

    var clone = this.entity.clone();
    clone.enabled = false;
    this.entity.parent.addChild(clone);

    // Get the parent's rotation
    var parentRotation = this.entity.getEulerAngles();

    // Determine the additional rotation from DNA
    var spreadAmountGenome = parseInt(this.dnaString.charAt(5), 10);
    var rotationMultiplier = spreadAmountGenome;
    var additionalRotation = new pc.Vec3(
        (Math.random() * 10 - 5) * rotationMultiplier, 
        (Math.random() * 10 - 5) * rotationMultiplier, 
        (Math.random() * 10 - 5) * rotationMultiplier
    );

    // Combine the rotations
    var combinedRotation = parentRotation.add(additionalRotation);
    clone.setEulerAngles(combinedRotation);

    setTimeout(function() { clone.enabled = true; }, 1000);
    var cloneLifeScript = clone.script.life;
    if (cloneLifeScript) {
        cloneLifeScript.dna = this.mutateDna(this.dna);
    }
};

Life.prototype.spreadAmountGenome = function() {
  var spreadAmountGenome = parseInt(this.dnaString.charAt(5), 10);
    var rotationMultiplier = spreadAmountGenome;
    var randomRotation = new pc.Vec3(
        (Math.random() * 50 - 5) * rotationMultiplier, 
        (Math.random() * 50 - 5) * rotationMultiplier, 
        (Math.random() * 50 - 5) * rotationMultiplier
    );
    clone.setEulerAngles(randomRotation);

    setTimeout(function() { clone.enabled = true; }, 1000);
    var cloneLifeScript = clone.script.life;
    if (cloneLifeScript) {
        cloneLifeScript.dna = this.mutateDna(this.dna);
    }
};

Life.prototype.startLifespanCountdown = function() {
    var lifespanGenome = parseInt(this.dnaString.charAt(4), 10); // Lifespan
    var lifespanDuration = lifespanGenome * 10;
    setTimeout(function() {
        if (this.entity) {
            this.entity.destroy();
        }
    }.bind(this), lifespanDuration * 1000);
};

Life.prototype.mutateDna = function(dna) {
    var dnaArray = dna.split('');
    for (var i = 0; i < this.mutationRate; i++) {
        var indexToMutate = Math.floor(Math.random() * dnaArray.length);
        var currentValue = parseInt(dnaArray[indexToMutate]);
        var mutationChance = Math.random();
        var mutation = mutationChance < 0.5 ? -1 : 1;
        var mutationScale = 0.1 + (this.mutationRate - 1) * 0.1;
        var newValue = currentValue + mutation * mutationScale;
        if (newValue < 1) newValue = 1;
        if (newValue > 9) newValue = 9;
        dnaArray[indexToMutate] = Math.round(newValue).toString();
    }
    return dnaArray.join('');
};

Life.prototype.destroy = function() {
    if (this.timerId) {
        clearInterval(this.timerId);
    }
    if (this.reproductionTimerId) {
        clearInterval(this.reproductionTimerId);
    }
};

If someone could help me ensure no clones are created when the max amount is reached, unless 1 organism dies, and 1 more is born to limit overpopulation. Thanks for reading, you can test the project here.

Here are a few screenshots from running the simulation for a bit:


Honestly, it looks quite nice when the density is lower, and adding grass and other small plants would make planets with life actually nice/beautiful to explore.


If I get this working, I may add it more as an option instead of default. I use terrain generator for planets terrain like rocks mountains etc, all these models are mine that I made for this specific project.

I instead just added a function for when a organism tried to clone to give it a 50% change of doing it, then a 25% of instead dying.