Refactoring
This commit is contained in:
parent
1771ab7d23
commit
7ce787f5db
@ -18,3 +18,6 @@ csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Limit the number of characters in a line
|
||||
max_line_length = 100 # This setting does not enforce it; it's a guideline.
|
||||
|
||||
[*.{cs,vb}]
|
||||
dotnet_diagnostic.IDE1006.severity = none
|
||||
@ -52,6 +52,7 @@
|
||||
<Compile Include="Assets/NanoBrain/NeuroidBehaviour.cs" />
|
||||
<Compile Include="Assets/NanoBrain/SensoryNeuroid.cs" />
|
||||
<Compile Include="Assets/Scenes/Boids/Scripts/SwarmControl.cs" />
|
||||
<Compile Include="Assets/NanoBrain/Perception.cs" />
|
||||
<Compile Include="Assets/Scenes/Boids/Scripts/Boid.cs" />
|
||||
<Compile Include="Assets/NanoBrain/Neuroid.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -43,8 +43,9 @@ public class GraphEditorWindow : EditorWindow {
|
||||
}
|
||||
|
||||
// If this neuroid is not visited while its output neuroid is visited
|
||||
if (!neuronVisited.Contains(neuroid) && (neuroid.outputNeuroid == null ||
|
||||
(neuronVisited.Contains(neuroid.outputNeuroid) && neuroid.outputNeuroid.layerIx == layerIx - 1))) {
|
||||
// Note: this does not yet work for multiple outputs yet (see the use of First())
|
||||
if (!neuronVisited.Contains(neuroid) && (neuroid.outputNeuroids.Count == 0 ||
|
||||
(neuronVisited.Contains(neuroid.outputNeuroids.First()) && neuroid.outputNeuroids.First().layerIx == layerIx - 1))) {
|
||||
// Add it to the next layer
|
||||
currentLayer.neuroids.Add(neuroid);
|
||||
neuroid.layerIx = layerIx;
|
||||
@ -54,7 +55,7 @@ public class GraphEditorWindow : EditorWindow {
|
||||
Vector2Int neuroidPosition = new(layerIx, neuroidIx);
|
||||
neuroidPositions[neuroid] = neuroidPosition;
|
||||
neuroidIx++;
|
||||
Debug.Log($"Layer {layerIx} neuron {neuroidIx} id {neuroid.id} {neuroid.name}");
|
||||
Debug.Log($"Layer {layerIx} neuron {neuroidIx} name {neuroid.name}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,9 +111,9 @@ public class GraphEditorWindow : EditorWindow {
|
||||
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
|
||||
|
||||
int i = 0;
|
||||
float inputSpacing = 200f / layerNeuroid.synapses.Count;
|
||||
float inputSpacing = 200f / layerNeuroid.newSynapses.Count;
|
||||
float inputMargin = 100 + inputSpacing / 2;
|
||||
foreach (Synapse synapse in layerNeuroid.synapses.Values) {
|
||||
foreach (Synapse synapse in layerNeuroid.newSynapses.Values) {
|
||||
if (synapse.neuroid != null) {
|
||||
if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
|
||||
|
||||
@ -144,7 +145,7 @@ public class GraphEditorWindow : EditorWindow {
|
||||
// Draw the tooltip
|
||||
GUIContent tooltip = new(
|
||||
$"{neuroid.name}" +
|
||||
$"\nsynapse count {neuroid.synapses.Count}" +
|
||||
$"\nsynapse count {neuroid.newSynapses.Count}" +
|
||||
$"\nValue: {neuroid.outputValue}" +
|
||||
$"\nStale: {neuroid.stale}");
|
||||
|
||||
|
||||
@ -29,17 +29,16 @@ public class NeuroidNetwork {
|
||||
}
|
||||
|
||||
public class Neuroid {
|
||||
public int id;
|
||||
//public int id;
|
||||
public string name;
|
||||
|
||||
public int layerIx;
|
||||
public int stale = 0;
|
||||
|
||||
public readonly Dictionary<int, Synapse> synapses = new();
|
||||
public readonly Dictionary<Neuroid, Synapse> newSynapses = new();
|
||||
|
||||
public Vector3 outputValue;
|
||||
public Neuroid outputNeuroid;
|
||||
public int outputNeurix;
|
||||
public HashSet<Neuroid> outputNeuroids = new();
|
||||
|
||||
public enum Mode {
|
||||
Sum,
|
||||
@ -52,69 +51,95 @@ public class Neuroid {
|
||||
|
||||
public Neuroid(NeuroidNetwork net) {
|
||||
this.net = net;
|
||||
this.net.neuroids.Add(this);
|
||||
if (this.net != null)
|
||||
this.net.neuroids.Add(this);
|
||||
}
|
||||
|
||||
public void SetOutputTo(Neuroid neuroid) {
|
||||
this.outputNeuroid = neuroid;
|
||||
// neuroid.inputNeuroids.Add(this);
|
||||
this.outputNeurix = this.id;
|
||||
public void AddSynapse(Neuroid input) {
|
||||
input.AddReceiver(this);
|
||||
this.newSynapses[input] = new(input, Vector3.zero, 1.0f);
|
||||
}
|
||||
|
||||
public void AddReceiver(Neuroid receiver) {
|
||||
this.outputNeuroids.Add(receiver);
|
||||
}
|
||||
|
||||
public void ResetWeights() {
|
||||
foreach (Synapse synapse in synapses.Values)
|
||||
foreach (Synapse synapse in this.newSynapses.Values)
|
||||
synapse.weight = 1.0f;
|
||||
}
|
||||
|
||||
public void SetWeight(Neuroid input, float weight) {
|
||||
if (synapses.ContainsKey(input.id))
|
||||
synapses[input.id] = new(input, synapses[input.id].value, weight);
|
||||
else
|
||||
synapses[input.id] = new(input, Vector3.zero, weight);
|
||||
if (this.newSynapses.ContainsKey(input)) {
|
||||
this.newSynapses[input].weight = weight;
|
||||
}
|
||||
else {
|
||||
this.newSynapses[input] = new(input, Vector3.zero, weight);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetInputFrom(Neuroid input, float weight = 1.0f) {
|
||||
input.id = this.synapses.Count;
|
||||
input.SetOutputTo(this);
|
||||
synapses[input.id] = new(input, Vector3.zero, weight);
|
||||
input.AddReceiver(this);
|
||||
this.newSynapses[input] = new(input, Vector3.zero, weight);
|
||||
}
|
||||
|
||||
public void SetInput(int inputId, Vector3 value) {
|
||||
if (synapses.ContainsKey(inputId))
|
||||
synapses[inputId].value = value;
|
||||
public void SetInput(Neuroid input, Vector3 value) {
|
||||
if (this.newSynapses.ContainsKey(input)) {
|
||||
Synapse synapse = this.newSynapses[input];
|
||||
synapse.value = value;
|
||||
}
|
||||
else
|
||||
synapses[inputId] = new(null, value, 1.0f);
|
||||
this.newSynapses[input] = new(null, value, 1.0f);
|
||||
UpdateState();
|
||||
}
|
||||
public void SetInput(int inputIx, Vector3 value, float weight) {
|
||||
if (synapses.ContainsKey(inputIx)) {
|
||||
Synapse synapse = synapses[inputIx];
|
||||
|
||||
public void SetInput(Neuroid input, Vector3 value, float weight) {
|
||||
if (this.newSynapses.ContainsKey(input)) {
|
||||
Synapse synapse = this.newSynapses[input];
|
||||
synapse.value = value;
|
||||
synapse.weight = weight;
|
||||
}
|
||||
else
|
||||
synapses[inputIx] = new(null, value, weight);
|
||||
this.newSynapses[input] = new(null, value, weight);
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void UpdateState() {
|
||||
public readonly Dictionary<int, Neuroid> fakeNeuroids = new();
|
||||
public void SetInput(int thingId, Vector3 value, float weight, NeuroidNetwork net) {
|
||||
if (fakeNeuroids.ContainsKey(thingId)) {
|
||||
Neuroid fakeInput = fakeNeuroids[thingId];
|
||||
Synapse synapse = this.newSynapses[fakeInput];
|
||||
synapse.value = value;
|
||||
synapse.weight = weight;
|
||||
}
|
||||
else {
|
||||
fakeNeuroids[thingId] = new(net);
|
||||
this.newSynapses[fakeNeuroids[thingId]] = new (null, value, weight);
|
||||
}
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
|
||||
protected virtual void UpdateState() {
|
||||
Vector3 sum = Vector3.zero;
|
||||
foreach (Synapse synapse in synapses.Values)
|
||||
foreach (Synapse synapse in this.newSynapses.Values)
|
||||
sum += synapse.value * synapse.weight;
|
||||
|
||||
this.outputValue = Activation(sum);
|
||||
this.outputNeuroid?.SetInput(this.outputNeurix, this.outputValue);
|
||||
foreach (Neuroid neuroid in outputNeuroids) {
|
||||
neuroid?.SetInput(this, this.outputValue);
|
||||
}
|
||||
this.stale = 0;
|
||||
}
|
||||
|
||||
Vector3 Activation(Vector3 sum) {
|
||||
if (synapses.Count == 0 && mode == Mode.Average)
|
||||
Debug.LogWarning($"{this.id} {this.name} has zero synapses for average");
|
||||
if (this.newSynapses.Count == 0 && mode == Mode.Average)
|
||||
Debug.LogWarning($"{this.name} has zero synapses for average");
|
||||
if (float.IsNaN(sum.magnitude))
|
||||
Debug.LogWarning($"{this.id} {this.name} sum is nan");
|
||||
Debug.LogWarning($"{this.name} sum is nan");
|
||||
return mode switch {
|
||||
Mode.Sum => sum,
|
||||
Mode.Average => sum / synapses.Count,
|
||||
Mode.Average => sum / this.newSynapses.Count,
|
||||
_ => sum,
|
||||
};
|
||||
//return sum; //(sum.magnitude > 0.5f) ? sum : Vector3.zero;
|
||||
|
||||
83
Assets/NanoBrain/Perception.cs
Normal file
83
Assets/NanoBrain/Perception.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class Perception {
|
||||
public SensoryNeuroid[] sensoryNeuroids = new SensoryNeuroid[7];
|
||||
//public Neuroid[] velocitySensors = new Neuroid[7];
|
||||
public NeuroidNetwork neuroidNet { get; protected set; }
|
||||
|
||||
public HashSet<Neuroid> receivers { get; protected set; }
|
||||
|
||||
public Perception(NeuroidNetwork neuroidNet) {
|
||||
this.neuroidNet = neuroidNet;
|
||||
this.receivers = new();
|
||||
}
|
||||
|
||||
// public void SendOutputTo(Neuroid receiver) {
|
||||
// foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
|
||||
// if (neuroid != null) {
|
||||
// neuroid.AddReceiver(receiver);
|
||||
// receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
public void SendPositions(Neuroid receiver) {
|
||||
receivers.Add(receiver);
|
||||
foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
|
||||
if (neuroid != null) {
|
||||
neuroid.AddReceiver(receiver);
|
||||
receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void SendVelocities(Neuroid receiver) {
|
||||
receivers.Add(receiver);
|
||||
foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
|
||||
if (neuroid != null && neuroid.velocityNeuroid != null) {
|
||||
neuroid.velocityNeuroid.AddReceiver(receiver);
|
||||
receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessStimulus(int thingId, Vector3 localPosition) {
|
||||
int availableIx = -1;
|
||||
SensoryNeuroid leastInterestingNeuroid = null;
|
||||
for (int i = 0; i < sensoryNeuroids.Length; i++) {
|
||||
if (sensoryNeuroids[i] == null || sensoryNeuroids[i].IsStale())
|
||||
availableIx = i;
|
||||
else if (sensoryNeuroids[i].receptor.thingId == thingId) {
|
||||
sensoryNeuroids[i].receptor.position = localPosition;
|
||||
return;
|
||||
}
|
||||
if (sensoryNeuroids[i] != null) {
|
||||
if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.position.magnitude > sensoryNeuroids[i].receptor.position.magnitude)
|
||||
leastInterestingNeuroid = sensoryNeuroids[i];
|
||||
}
|
||||
}
|
||||
if (availableIx != -1) {
|
||||
if (sensoryNeuroids[availableIx] != null) {
|
||||
// Debug.Log($"revived receptor {availableIx} for {thingId}");
|
||||
sensoryNeuroids[availableIx].receptor.thingId = thingId;
|
||||
sensoryNeuroids[availableIx].receptor.position = localPosition;
|
||||
}
|
||||
else {
|
||||
// Debug.Log($"new receptor for {thingId}");
|
||||
SensoryNeuroid neuroid = new(neuroidNet, thingId);
|
||||
foreach (Neuroid receiver in receivers)
|
||||
receiver.GetInputFrom(neuroid);
|
||||
|
||||
sensoryNeuroids[availableIx] = neuroid;
|
||||
neuroid.receptor.position = localPosition;
|
||||
}
|
||||
}
|
||||
else if (leastInterestingNeuroid != null) {
|
||||
//Debug.Log($"replaced receptor {leastInterestingNeuroid.thingId} for {thingId}");
|
||||
leastInterestingNeuroid.receptor.thingId = thingId;
|
||||
leastInterestingNeuroid.receptor.position = localPosition;
|
||||
}
|
||||
|
||||
//Debug.LogWarning($"No available receptor for {id}");
|
||||
}
|
||||
}
|
||||
2
Assets/NanoBrain/Perception.cs.meta
Normal file
2
Assets/NanoBrain/Perception.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37d94d399d30e6eb996236adabad87ee
|
||||
@ -1,32 +1,70 @@
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
public class Receptor {
|
||||
public SensoryNeuroid neuroid;
|
||||
public void SetValue(Vector3 value) {
|
||||
if (neuroid != null) {
|
||||
neuroid.SetInput(neuroid.id, value);
|
||||
}
|
||||
public class SensoryNeuroid : Neuroid {
|
||||
// A neuroid which has no neurons as input
|
||||
// But receives value from a receptor
|
||||
public Receptor receptor;
|
||||
public VelocityNeuroid velocityNeuroid;
|
||||
|
||||
public SensoryNeuroid(NeuroidNetwork net, int thingId) : base(net) {
|
||||
this.name = "sensory neuroid";
|
||||
this.receptor = new Receptor {
|
||||
neuroid = this,
|
||||
thingId = thingId
|
||||
};
|
||||
this.velocityNeuroid = new(net);
|
||||
// The velocity neuroid received position data from this
|
||||
this.AddReceiver(velocityNeuroid);
|
||||
}
|
||||
public Vector3 GetValue() {
|
||||
if (neuroid != null)
|
||||
return neuroid.synapses[neuroid.id].value;
|
||||
else
|
||||
return Vector3.zero;
|
||||
|
||||
}
|
||||
|
||||
public class Receptor {
|
||||
|
||||
public SensoryNeuroid neuroid;
|
||||
|
||||
public int thingId;
|
||||
/// <summary>
|
||||
/// Local position of the thing
|
||||
/// </summary>
|
||||
public virtual Vector3 position {
|
||||
get {
|
||||
if (neuroid != null)
|
||||
return neuroid.newSynapses[neuroid].value;
|
||||
else
|
||||
return Vector3.zero;
|
||||
}
|
||||
set {
|
||||
if (neuroid != null)
|
||||
neuroid.SetInput(neuroid, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class VelocityNeuroid : Neuroid {
|
||||
// Would be best if this was received through a synapse via a loop....
|
||||
private Vector3 lastPosition = Vector3.zero;
|
||||
private float lastValueTime = 0;
|
||||
|
||||
public class SensoryNeuroid : Neuroid {
|
||||
public Receptor receptor;
|
||||
public int thingId;
|
||||
|
||||
public SensoryNeuroid(NeuroidNetwork net, int id) : base(net) {
|
||||
this.name = "sensory neuroid";
|
||||
// this.id = id;
|
||||
this.thingId = id;
|
||||
this.receptor = new Receptor {
|
||||
neuroid = this
|
||||
};
|
||||
public VelocityNeuroid(NeuroidNetwork net) : base(net) {
|
||||
}
|
||||
|
||||
protected override void UpdateState() {
|
||||
// Assuming only one synapse for now....
|
||||
Vector3 currentPosition = this.newSynapses.First().Value.value;
|
||||
float currentValueTime = Time.time;
|
||||
|
||||
float deltaTime = currentValueTime - lastValueTime;
|
||||
Vector3 translation = currentPosition - lastPosition;
|
||||
Vector3 velocity = translation / deltaTime;
|
||||
|
||||
// No activation function...
|
||||
this.outputValue = velocity;
|
||||
foreach (Neuroid receiver in outputNeuroids)
|
||||
receiver?.SetInput(this, this.outputValue);
|
||||
this.stale = 0;
|
||||
|
||||
this.lastValueTime = Time.time;
|
||||
}
|
||||
}
|
||||
@ -378,7 +378,7 @@ MonoBehaviour:
|
||||
separationForce: 5
|
||||
separationDistance: 0.3
|
||||
bodyForce: 20
|
||||
perceptionDistance: 2
|
||||
perceptionDistance: 1
|
||||
boundaryForce: 5
|
||||
spaceSize: {x: 10, y: 10, z: 10}
|
||||
boundaryWidth: {x: 1, y: 1, z: 1}
|
||||
@ -394,7 +394,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: ec888ca5333d45a438f9f417fa5ce135, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::SwarmSpawn
|
||||
count: 1000
|
||||
count: 5
|
||||
boidPrefab: {fileID: 8702527964058765413, guid: f9c706268554ce449a8773675b2864b8, type: 3}
|
||||
spawnAreaSize: {x: 0.5, y: 0.5, z: 0.5}
|
||||
minDelay: 0.05
|
||||
|
||||
@ -19,7 +19,8 @@ public class Boid : MonoBehaviour {
|
||||
|
||||
readonly Collider[] results = new Collider[10];
|
||||
|
||||
public SensoryNeuroid[] neighbourSensor = new SensoryNeuroid[6];
|
||||
//public SensoryNeuroid[] neighbourSensor = new SensoryNeuroid[6];
|
||||
public Perception perception;
|
||||
|
||||
public NeuroidNetwork neuroidNet = new();
|
||||
public Neuroid bodyVector;
|
||||
@ -40,11 +41,14 @@ public class Boid : MonoBehaviour {
|
||||
|
||||
bounds = new(sc.transform.position, sc.spaceSize - 2 * sc.boundaryWidth);
|
||||
|
||||
perception = new Perception(neuroidNet);
|
||||
//neighbourSensor = new(neuroidNet) { name = "Neighbour", id = 879 };
|
||||
|
||||
cohesion = new(neuroidNet) { name = "Cohesion", mode = Neuroid.Mode.Sum };
|
||||
perception.SendPositions(cohesion);
|
||||
//cohesion.GetInputFrom(neighbourSensor);
|
||||
alignment = new(neuroidNet) { name = "Alignment", mode = Neuroid.Mode.Average };
|
||||
//perception.SendVelocities(alignment);
|
||||
separation = new(neuroidNet) { name = "Separation", mode = Neuroid.Mode.Sum };
|
||||
target = new(neuroidNet) { name = "Target", mode = Neuroid.Mode.Sum };
|
||||
boundary = new(neuroidNet) { name = "Boundary", mode = Neuroid.Mode.Sum };
|
||||
@ -78,15 +82,15 @@ public class Boid : MonoBehaviour {
|
||||
Vector3 localPosition = neighbour.transform.position - this.transform.position;
|
||||
Vector3 relativeVelocity = neighbour.velocity - this.velocity;
|
||||
|
||||
int id = neighbour.GetInstanceID();
|
||||
ProcessStimulus(id, localPosition);
|
||||
int thingId = neighbour.GetInstanceID();
|
||||
perception.ProcessStimulus(thingId, localPosition);
|
||||
|
||||
Vector3 separationForce = -localPosition / localPosition.sqrMagnitude;
|
||||
// which is equivalent to -(localPosition.normalized / localPosition.magnitude)
|
||||
|
||||
separation.SetInput(id, separationForce, sc.separationDistance);
|
||||
//cohesion.SetInput(id, localPosition, sc.cohesionForce);
|
||||
alignment.SetInput(id, relativeVelocity, sc.alignmentForce);
|
||||
separation.SetInput(thingId, separationForce, sc.separationDistance, neuroidNet);
|
||||
//cohesion.SetInput(thingId, localPosition, sc.cohesionForce);
|
||||
alignment.SetInput(thingId, relativeVelocity, sc.alignmentForce, neuroidNet);
|
||||
neighbourCount++;
|
||||
|
||||
}
|
||||
@ -108,7 +112,7 @@ public class Boid : MonoBehaviour {
|
||||
Vector3 direction = (sc.transform.position - this.transform.position).normalized;
|
||||
outside = direction * magnitude;
|
||||
|
||||
boundary.SetInput(id, outside, sc.boundaryForce);
|
||||
boundary.SetInput(id, outside, sc.boundaryForce, neuroidNet);
|
||||
// Debug.Log($"boundary {this.transform.position} {outside} force = {outside * sc.boundaryForce}");
|
||||
}
|
||||
|
||||
@ -128,7 +132,7 @@ public class Boid : MonoBehaviour {
|
||||
//Debug.Log($"neighbours: {neighbourCount} synapses: {cohesion.synapses.Count}");
|
||||
neuroidNet.Update();
|
||||
}
|
||||
|
||||
/*
|
||||
Receptor GetReceptor(Neuroid perceptionNeuroid, int id) {
|
||||
int availableIx = -1;
|
||||
for (int i = 0; i < neighbourSensor.Length; i++) {
|
||||
@ -157,7 +161,7 @@ public class Boid : MonoBehaviour {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
void ProcessStimulus(int thingId, Vector3 value) {
|
||||
int availableIx = -1;
|
||||
SensoryNeuroid leastInterestingNeuroid = null;
|
||||
@ -168,8 +172,6 @@ public class Boid : MonoBehaviour {
|
||||
neighbourSensor[i].receptor.SetValue(value);
|
||||
return;
|
||||
}
|
||||
// if (leastInterestingIx == -1 || neighbourSensor[leastInterestingIx].receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude)
|
||||
// leastInterestingIx = i;
|
||||
if (neighbourSensor[i] != null) {
|
||||
if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude)
|
||||
leastInterestingNeuroid = neighbourSensor[i];
|
||||
@ -197,7 +199,7 @@ public class Boid : MonoBehaviour {
|
||||
|
||||
//Debug.LogWarning($"No available receptor for {id}");
|
||||
}
|
||||
|
||||
*/
|
||||
void OnDrawGizmosSelected() {
|
||||
Gizmos.DrawWireSphere(transform.position, sc.perceptionDistance);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user