WIP: Initial scripts

This commit is contained in:
Pascal Serrarens 2026-03-09 11:10:17 +01:00
parent 2219e9860c
commit d9ba98da47
23 changed files with 557 additions and 0 deletions

5
.gitignore vendored
View File

@ -26,6 +26,7 @@
# Visual Studio cache directory # Visual Studio cache directory
.vs/ .vs/
.vscode/
# Gradle cache directory # Gradle cache directory
.gradle/ .gradle/
@ -73,3 +74,7 @@ crashlytics-build.properties
/[Aa]ssets/[Ss]treamingAssets/aa.meta /[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/* /[Aa]ssets/[Ss]treamingAssets/aa/*
# Passer
/Samples
/Samples.meta
/Samples~.meta

8
Editor.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9d715489a5eda538a959b6e4cb89d1bc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

7
LICENSE.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5099850b415a5749b9205723281be597
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

7
README.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d1f9abd0bb6ced431ad732ea64b1d722
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Runtime.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c85913106343e6988b160abd2d543f3f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Runtime/Scripts.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94025bfe58d21d7aba189dab371f3e97
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,245 @@
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(NanoBrain))]
public class AnimatedAnt : MonoBehaviour {
private readonly float inertia = 0.2f;
private readonly float smellRadius = 1.0f;
private readonly float smellAngle = 80.0f;
public GameObject homePheromonePrefab;
public GameObject foodPheromonePrefab;
public AntennaTouch touchLeft;
public AntennaTouch touchRight;
public NanoBrain nanoBrain;
// brain output
public Neuron hasFood;
// brain input
public Nucleus pheromoneSteering;
public Nucleus hitLeft;
public Nucleus hitRight;
public Nucleus beat;
public Receptor foodReceptor;
public Receptor homeReceptor;
public new Rigidbody rigidbody;
public Animator animator;
public float forwardSpeed = 1;
public float rotationSpeed = 1;
public Vector3 linearVelocity;
public Vector3 angularVelocity;
protected virtual void Awake() {
this.animator = GetComponentInChildren<Animator>();
if (this.animator == null && this.transform.parent != null)
this.animator = this.transform.parent.GetComponentInChildren<Animator>();
this.nanoBrain = GetComponentInChildren<NanoBrain>();
this.rigidbody = GetComponentInParent<Rigidbody>();
this.rigidbody.isKinematic = false;
}
void Start() {
Cluster brain = this.nanoBrain.brain;
if (brain != null) {
// brain outputs
this.pheromoneSteering = brain.GetNucleus("Pheromone Steering");
if (brain.GetNucleus("Home Pheromones") is Neuron homePheromones)
homePheromones.WhenFiring += PlaceHomePheromone;
if (brain.GetNucleus("Food Pheromones") is Neuron foodPheromones)
foodPheromones.WhenFiring += PlaceFoodPheromone;
this.hasFood = brain.GetNucleus("Having Food") as Neuron;
// brain inputs
this.beat = brain.GetNucleus("Beat");
this.hitLeft = brain.GetNucleus("Hit Left");
this.hitRight = brain.GetNucleus("Hit Right");
this.foodReceptor = brain.GetNucleus("Food Receptor") as Receptor;
this.homeReceptor = brain.GetNucleus("Home Receptor") as Receptor;
}
this.linearVelocity = Vector3.forward;
if (touchLeft != null)
touchLeft.touched += OnAntennaTouchLeft;
if (touchRight != null)
touchRight.touched += OnAntennaTouchRight;
}
void PlaceFoodPheromone() {
GameObject pheromoneObj = Instantiate(foodPheromonePrefab);
pheromoneObj.transform.position = this.transform.position;
}
void PlaceHomePheromone() {
GameObject pheromoneObj = Instantiate(homePheromonePrefab);
pheromoneObj.transform.position = this.transform.position;
}
// Update is called once per frame
void Update() {
UpdateBeat();
UpdateSmell();
if (this.nanoBrain == null || this.nanoBrain.brain == null || this.animator == null)
return;
Vector3 localForce = nanoBrain.brain.defaultOutput.outputValue;
this.linearVelocity = (1 - inertia) * (Time.deltaTime * localForce.normalized) + inertia * this.linearVelocity;
this.linearVelocity = this.linearVelocity.normalized * 0.2f;
// Vector3 linearVelocityWorld = this.transform.TransformVector(this.linearVelocity);
// this.rigidbody.linearVelocity = linearVelocityWorld;
this.animator.SetFloat("forward speed", this.linearVelocity.z * this.forwardSpeed);
// Rotate towards the movement direction
if (this.linearVelocity != Vector3.zero) {
Quaternion targetRotation = Quaternion.LookRotation(this.linearVelocity);
Quaternion worldRotation = transform.rotation * targetRotation;
Quaternion deltaRotation = worldRotation * Quaternion.Inverse(transform.rotation);
Vector3 eulerAngleChange = deltaRotation.eulerAngles;
// Normalize the Euler angles to avoid unexpected jumps due to 360-degree rotations
eulerAngleChange = new Vector3(
LinearAlgebra.Angles.Normalize(eulerAngleChange.x),
LinearAlgebra.Angles.Normalize(eulerAngleChange.y),
LinearAlgebra.Angles.Normalize(eulerAngleChange.z)
);
Vector3 angularVelocity = 5f * Mathf.Deg2Rad * eulerAngleChange;
//rigidbody.angularVelocity = angularVelocity;
this.animator.SetFloat("rotate speed", eulerAngleChange.y / 45 * this.rotationSpeed);
}
}
public float beatInterval = 3;
float lastBeatTime = 0;
void UpdateBeat() {
if (lastBeatTime == 0) {
ulong delay = (ulong)(UnityEngine.Random.value * beatInterval);
lastBeatTime = Time.time - delay;
}
if (Time.time - lastBeatTime >= beatInterval) {
lastBeatTime = Time.time;
beat?.SetBias(Vector3.one); //, 0);
}
}
void UpdateSmell() {
// To generate random basic movement, we add a small with a random direction with low intensity
float randomAngle = Random.Range(-smellAngle, smellAngle);
Vector3 randomDirection = Quaternion.AngleAxis(randomAngle, Vector3.up) * Vector3.forward * 0.01f;
pheromoneSteering?.SetBias(randomDirection); //, 0, "random");
Collider[] colliders = Physics.OverlapSphere(this.transform.position, smellRadius);
foreach (Collider collider in colliders) {
SmellPheromones(collider);
SmellFood(collider);
SmellHome(collider);
}
if (nanoBrain != null && nanoBrain.brain != null)
nanoBrain.brain.UpdateNuclei();
}
void SmellPheromones(Collider thing) {
Pheromone pheromone = thing.GetComponentInParent<Pheromone>();
if (pheromone == null)
return;
// if (hasFood == null)
// return;
// if ((hasFood.outputValue.x > 0 && pheromone.type == Pheromone.Type.Home) ||
// (hasFood.outputValue.x < 0 && pheromone.type == Pheromone.Type.Food)) {
// Vector3 smellDirection = this.transform.InverseTransformPoint(pheromone.transform.position);
// float distance = smellDirection.magnitude;
// float angle = Vector3.Angle(Vector3.forward, smellDirection);
// if (angle < smellAngle && smellDirection.magnitude > 0.05) {
// float intensity = pheromone.StrengthAt(distance);//strength * (1 / distance);
// pheromoneSteering?.ProcessStimulus(pheromone.GetInstanceID(), smellDirection.normalized * intensity,
// pheromone.type.ToString() + " pheromone");
// //Debug.DrawLine(this.transform.position, pheromone.transform.position, Color.magenta);
// }
// }
Vector3 smellDirection = this.transform.InverseTransformPoint(pheromone.transform.position);
float distance = smellDirection.magnitude;
float angle = Vector3.Angle(Vector3.forward, smellDirection);
if (angle < smellAngle && smellDirection.magnitude > 0.05) {
float intensity = pheromone.StrengthAt(distance);
Vector3 smell = smellDirection.normalized * intensity;
switch (pheromone.type) {
case Pheromone.Type.Food:
//foodSmell?.ProcessStimulus(pheromone.GetInstanceID(), smell, "food pheromone");
foodReceptor?.ProcessStimulus(smellDirection.normalized * intensity, pheromone.GetInstanceID(), "food pheromone");
break;
case Pheromone.Type.Home:
//pheromoneSteering?.ProcessStimulus(pheromone.GetInstanceID(), smell, "home pheromone");
//homeSmell?.ProcessStimulus(pheromone.GetInstanceID(), smell, "home pheromone");
homeReceptor?.ProcessStimulus(smellDirection.normalized * intensity, pheromone.GetInstanceID(), "home pheromone");
break;
}
//Debug.DrawLine(this.transform.position, pheromone.transform.position, Color.magenta);
}
}
void SmellFood(Collider thing) {
if (hasFood != null && hasFood.outputValue.x > 0)
// if it has food...
return;
Food food = thing.GetComponentInParent<Food>();
if (food == null)
return;
Vector3 smellDirection = this.transform.InverseTransformPoint(food.transform.position);
float distance = smellDirection.magnitude;
float angle = Vector3.Angle(Vector3.forward, smellDirection);
if (angle < smellAngle && distance > 0.05) {
float intensity = food.StrengthAt(distance); //strength * (1 / distance);
//foodSmell?.ProcessStimulus(food.GetInstanceID(), smellDirection.normalized * intensity, "food");
foodReceptor?.ProcessStimulus(smellDirection.normalized * intensity, food.GetInstanceID(), "food");
Debug.DrawLine(this.transform.position, food.transform.position, Color.red);
}
}
void SmellHome(Collider thing) {
if (hasFood != null && hasFood.outputValue.x < 0)
// if it does not have food....
return;
AntsNest nest = thing.GetComponentInParent<AntsNest>();
if (nest == null)
return;
Vector3 smellDirection = this.transform.InverseTransformPoint(nest.transform.position);
float distance = smellDirection.magnitude;
float angle = Vector3.Angle(Vector3.forward, smellDirection);
if (angle < smellAngle && distance > 0.05) {
float intensity = nest.StrengthAt(distance); //strength * (1 / distance);
//homeSmell?.ProcessStimulus(nest.GetInstanceID(), smellDirection.normalized * intensity, "nest");
Vector3 value = smellDirection.normalized * intensity;
homeReceptor?.ProcessStimulus(value, nest.GetInstanceID(), "nest");
Debug.DrawLine(this.transform.position, nest.transform.position, Color.red);
}
}
void OnAntennaTouchLeft(Collider other, bool isTouching) {
Vector3 touchDirection = Vector3.zero;
if (isTouching)
touchDirection = this.transform.InverseTransformVector(touchLeft.transform.forward);
hitLeft?.SetBias(touchDirection); //, other.GetInstanceID(), "Touch Left");
}
void OnAntennaTouchRight(Collider other, bool isTouching) {
Vector3 touchDirection = Vector3.zero;
if (isTouching)
touchDirection = this.transform.InverseTransformVector(touchRight.transform.forward);
hitRight?.SetBias(touchDirection); //, other.GetInstanceID(), "Touch Right");
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 456c8036e1196d245a3894315f92807d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
using System;
using UnityEngine;
public class AntennaTouch : MonoBehaviour
{
public Action<Collider, bool> touched;
// void OnTriggerEnter(Collider other) {
// touched?.Invoke(other, true);
// }
void OnTriggerStay(Collider other) {
touched?.Invoke(other, true);
}
void OnTriggerExit(Collider other) {
touched?.Invoke(other, false);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62f6712fb1e4fb40285b0bb5008716dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,32 @@
using System.Collections;
using UnityEngine;
public class AntsNest : Odorant {
public Ant antPrefab;
public uint numberOfAnts = 1;
public bool spawnAnt = false;
private uint antCount = 0;
protected virtual void Start() {
StartCoroutine(SpawnAnts());
}
IEnumerator SpawnAnts() {
while (numberOfAnts > 0) {
Ant ant = Instantiate(antPrefab);
ant.transform.eulerAngles = 360 * Random.value * Vector3.up;
ant.transform.position = this.transform.position + ant.transform.forward * 0.1F;
ant.name = "Ant " + (++antCount);
numberOfAnts--;
yield return new WaitForSeconds(0.2f);
}
}
protected virtual void Update() {
if (spawnAnt) {
Ant ant = Instantiate(antPrefab);
ant.name = "Ant " + (++antCount);
spawnAnt = false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d0564480d9177a79bba1b4a7d666d26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

5
Runtime/Scripts/Food.cs Normal file
View File

@ -0,0 +1,5 @@
using UnityEngine;
public class Food : Odorant
{
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a250a4dd786bd61cb1ea1f3618db2ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

64
Runtime/Scripts/Mouth.cs Normal file
View File

@ -0,0 +1,64 @@
using System.Collections.Generic;
using UnityEngine;
public class Mouth : MonoBehaviour {
public GameObject foodPrefab;
public NanoBrain nanoBrain;
public Neuron havingFood;
public Nucleus enableFoodPheromones;
void Awake() {
this.nanoBrain = GetComponentInParent<NanoBrain>();
if (this.nanoBrain == null || this.nanoBrain.brain == null)
return;
if (nanoBrain.brain.GetNucleus("Mouth") is Neuron mouthNeuron)
mouthNeuron.WhenFiring += CheckGrab;
this.havingFood = nanoBrain.brain.GetNucleus("Having Food") as Neuron;
}
void Start() {
havingFood?.SetBias(-Vector3.one);
}
protected void CheckGrab() {
if (havingFood == null)
return;
Collider[] colliders = Physics.OverlapSphere(this.transform.position, 0.04f);
foreach (Collider c in colliders) {
if (havingFood.outputValue.x > 0) {
AntsNest nest = c.GetComponentInParent<AntsNest>();
if (nest != null)
LetGo();
}
else {
Food food = c.GetComponentInParent<Food>();
if (food != null)
Grab();
}
}
}
public bool Grab() {
//Debug.Log($"{this.transform.parent.name} Grab food");
havingFood?.SetBias(Vector3.one);
GameObject food = Instantiate(foodPrefab);
food.transform.SetParent(this.transform, false);
food.transform.localPosition = Vector3.zero;
return true;
}
public void LetGo() {
//Debug.Log($"{this.transform.parent.name} Release food");
List<GameObject> toDelete = new();
for (int i = 0; i < transform.childCount; i++)
toDelete.Add(transform.GetChild(i).gameObject);
foreach (GameObject go in toDelete)
Destroy(go);
havingFood?.SetBias(-Vector3.one);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 122a8c835cdbd95fb87b06d19573b3da
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@
using UnityEngine;
public class Odorant : MonoBehaviour {
public float strength = 1;
public float StrengthAt(float distance) {
float intensity = this.strength * (1 / distance);
return intensity;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5715f90d63da4f8cc878030dce3e5c4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,22 @@
using UnityEngine;
public class Pheromone : Odorant {
//public float strength = 30; // seconds
public float duration = 30;
public enum Type {
Unknown = 0,
Food = 82,
Home = 83
};
public Type type = 0;
// Update is called once per frame
void Update() {
this.strength -= (Time.deltaTime / duration);
if (this.strength < 0) {
// Debug.Log($"destroyed {this.name}");
Destroy(this.gameObject);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8fa1bfe8c0b8b03e68a7557b345340c3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Tests.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 141272d99626acc4a8055d90fa390dee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "life.passer.ant",
"version": "0.0.1",
"displayName": "NanoBrain Ant",
"description": "An 3D ant model with behaviour controlled by a neural network",
"unity": "2022.3",
"author": {
"name": "Passer Life",
"email": "support@passer.life",
"url": "https://passer.life"
},
"documentationUrl": "https://passer.life/docs/Ant/Unity/index.html",
"changelogUrl": "https://git.passer.life/CreatureControl/Ant/releases",
"license": "MPL-2.0",
"licensesUrl": "https://git.passer.life/CreatureControl/Ant/src/branch/main/LICENSE",
"keywords": [
"creatire",
"insect",
"ant",
"animation",
"ai",
"behaviour", "behavior",
"neuron", "neural network"
],
"samples": [
]
}

7
package.json.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e422443485922f791a702b10ce70c06b
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: