Basic universe management, generation and simulation flow #17

Merged
aslan merged 1 commit from features/jant/14-basic-universe into main 2026-01-26 16:22:17 +01:00
29 changed files with 664 additions and 29 deletions

19
prefabs/star.tscn Normal file
View file

@ -0,0 +1,19 @@
[gd_scene format=3 uid="uid://cwgcue41fpm6j"]
[ext_resource type="Script" uid="uid://cbt6p1bhh4o8q" path="res://scripts/GameObjects/Nodes/StarNode.cs" id="1_o2f8k"]
[sub_resource type="SphereMesh" id="SphereMesh_o2f8k"]
radius = 5.0
height = 10.0
[sub_resource type="SphereShape3D" id="SphereShape3D_o2f8k"]
radius = 5.0
[node name="Star" type="StaticBody3D" unique_id=1306600716]
script = ExtResource("1_o2f8k")
[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1423841842]
mesh = SubResource("SphereMesh_o2f8k")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1898956937]
shape = SubResource("SphereShape3D_o2f8k")

View file

@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://bhb866m25nc8k"] [gd_scene format=3 uid="uid://bhb866m25nc8k"]
[ext_resource type="Script" uid="uid://dvd8i36joinsc" path="res://scripts/VesselNode.cs" id="1_d8j6t"] [ext_resource type="Script" uid="uid://dvd8i36joinsc" path="res://scripts/GameObjects/Nodes/VesselNode.cs" id="1_d8j6t"]
[node name="Vessel" type="RigidBody3D" unique_id=817832939] [node name="Vessel" type="RigidBody3D" unique_id=817832939]
mass = 1000.0 mass = 1000.0

View file

@ -3,7 +3,7 @@
[ext_resource type="Script" uid="uid://cue3c56axvyja" path="res://scripts/Player.cs" id="1_uwrxv"] [ext_resource type="Script" uid="uid://cue3c56axvyja" path="res://scripts/Player.cs" id="1_uwrxv"]
[ext_resource type="Script" uid="uid://cy8nuarxbnd" path="res://scripts/GravityZone.cs" id="1_yqjtg"] [ext_resource type="Script" uid="uid://cy8nuarxbnd" path="res://scripts/GravityZone.cs" id="1_yqjtg"]
[ext_resource type="Script" uid="uid://bwpdtkgmwjs7g" path="res://scripts/GravityReceiver.cs" id="3_lnu2h"] [ext_resource type="Script" uid="uid://bwpdtkgmwjs7g" path="res://scripts/GravityReceiver.cs" id="3_lnu2h"]
[ext_resource type="Script" uid="uid://n557xfrv0i6x" path="res://scripts/GeneralControlManager.cs" id="4_lbhrr"] [ext_resource type="Script" uid="uid://n557xfrv0i6x" path="res://scripts/GameControlManager.cs" id="4_lbhrr"]
[ext_resource type="Script" uid="uid://bwgjvm21oi3d6" path="res://scripts/GameManager.cs" id="4_p57ef"] [ext_resource type="Script" uid="uid://bwgjvm21oi3d6" path="res://scripts/GameManager.cs" id="4_p57ef"]
[ext_resource type="Script" uid="uid://betypbypf6bf2" path="res://scripts/GameMenuController.cs" id="5_iywne"] [ext_resource type="Script" uid="uid://betypbypf6bf2" path="res://scripts/GameMenuController.cs" id="5_iywne"]
@ -64,10 +64,11 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6497431, -4.41529)
omni_range = 500.0 omni_range = 500.0
omni_attenuation = 0.1 omni_attenuation = 0.1
[node name="GameManager" type="Node" parent="." unique_id=267630256] [node name="GameManager" type="Node" parent="." unique_id=267630256 node_paths=PackedStringArray("player")]
script = ExtResource("4_p57ef") script = ExtResource("4_p57ef")
player = NodePath("../Player")
[node name="GeneralControlManager" type="Node" parent="." unique_id=1751385863 node_paths=PackedStringArray("GameMenu")] [node name="GameControlManager" type="Node" parent="." unique_id=1751385863 node_paths=PackedStringArray("GameMenu")]
script = ExtResource("4_lbhrr") script = ExtResource("4_lbhrr")
GameMenu = NodePath("../GameMenu") GameMenu = NodePath("../GameMenu")

View file

@ -1,3 +1,3 @@
public static partial class Constants public static class Constants
{ {
} }

49
scripts/FastUniqueList.cs Normal file
View file

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
public class FastUniqueList<T>
{
private readonly List<T> list = [];
private readonly HashSet<T> set = [];
public int Count => list.Count;
public T this[int index] => list[index];
public bool Contains(T item)
{
return set.Contains(item);
}
public bool Add(T item)
{
if (!set.Add(item))
{
return false;
}
list.Add(item);
return true;
}
public bool Remove(T item)
{
if (!set.Remove(item))
{
return false;
}
int index = list.IndexOf(item);
int last = list.Count - 1;
list[index] = list[last];
list.RemoveAt(last);
return true;
}
public void ForEach(Action<T> action)
{
list.ForEach(action);
}
}

View file

@ -0,0 +1 @@
uid://sff5f644fn3q

View file

@ -1,6 +1,6 @@
using Godot; using Godot;
public partial class GeneralControlManager : Node public partial class GameControlManager : Node
{ {
[Export] Control GameMenu; [Export] Control GameMenu;

View file

@ -1,12 +1,148 @@
using System.Threading;
using System.Threading.Tasks;
using Godot; using Godot;
public partial class GameManager : Node public partial class GameManager : Node
{ {
public override void _Ready() [Export] Player player;
{
PackedScene shipPrefab = ResourceLoader.Load<PackedScene>("res://prefabs/vessel.tscn");
Node3D shipInstance = shipPrefab.Instantiate<Node3D>();
GetTree().CurrentScene.CallDeferred("add_child", shipInstance); [Export] double closeTickInterval = 1;
} [Export] double farTickInterval = 10;
[Export] int maxFarThreads = 16;
public static GameManager Singleton { get; private set; }
public IGenerator Generator { get; private set; }
public Universe GameUniverse { get; private set; }
public Sector CurrentSector { get; private set; }
private double closeTickTimer = 0;
private double farTickTimer = 0;
public override void _Ready()
{
Singleton = this;
Generator = new TestGenerator();
Generator.GenerateUniverse((universe) =>
{
GameUniverse = universe;
CurrentSector = universe.Sectors[1, 1, 1];
});
}
public override void _Process(double delta)
{
closeTickTimer += delta;
farTickTimer += delta;
if (closeTickTimer >= closeTickInterval)
{
SimulateClose(closeTickTimer);
closeTickTimer = 0;
}
if (farTickTimer >= farTickInterval)
{
SimulateFar(farTickTimer);
farTickTimer = 0;
}
}
private void SimulateClose(double delta)
{
Vector3I currentCoordinates = CurrentSector.Coordinates;
Sector[,,] sectors = GameUniverse.Sectors;
int sizeX = sectors.GetLength(0);
int sizeY = sectors.GetLength(1);
int sizeZ = sectors.GetLength(2);
int startX = Mathf.Clamp(currentCoordinates.X - 1, 0, sizeX);
int startY = Mathf.Clamp(currentCoordinates.Y - 1, 0, sizeY);
int startZ = Mathf.Clamp(currentCoordinates.Z - 1, 0, sizeZ);
int endX = Mathf.Clamp(currentCoordinates.X + 1, 0, sizeX);
int endY = Mathf.Clamp(currentCoordinates.Y + 1, 0, sizeY);
int endZ = Mathf.Clamp(currentCoordinates.Z + 1, 0, sizeZ);
for (int x = startX; x <= endX; x++)
{
for (int y = startY; y <= endY; y++)
{
for (int z = startZ; z <= endZ; z++)
{
int taskX = x;
int taskY = y;
int taskZ = z;
Task.Run(() =>
{
sectors[taskX, taskY, taskZ].Simulate(delta);
});
}
}
}
}
private void SimulateFar(double delta)
{
Sector[,,] sectors = GameUniverse.Sectors;
int sizeX = sectors.GetLength(0);
int countX = Mathf.Clamp(sizeX / maxFarThreads, 1, int.MaxValue);
for (int x = 0; x < sizeX; x += countX)
{
double taskDelta = delta;
int taskStartX = x;
int taskCountX = countX;
_ = SimulateFarThreaded(taskDelta, taskStartX, taskCountX);
}
}
private async Task SimulateFarThreaded(double delta, int startX, int countX)
{
Vector3I currentCoordinates = CurrentSector.Coordinates;
Sector[,,] sectors = GameUniverse.Sectors;
int sizeX = sectors.GetLength(0);
int sizeY = sectors.GetLength(1);
int sizeZ = sectors.GetLength(2);
int startSkipX = Mathf.Clamp(currentCoordinates.X - 1, 0, sizeX);
int startSkipY = Mathf.Clamp(currentCoordinates.Y - 1, 0, sizeY);
int startSkipZ = Mathf.Clamp(currentCoordinates.Z - 1, 0, sizeZ);
int endSkipX = Mathf.Clamp(currentCoordinates.X + 1, 0, sizeX);
int endSkipY = Mathf.Clamp(currentCoordinates.Y + 1, 0, sizeY);
int endSkipZ = Mathf.Clamp(currentCoordinates.Z + 1, 0, sizeZ);
int endX = Mathf.Clamp(startX + countX, 0, sizeX);
for (int x = startX; x < endX; x++)
{
for (int y = 0; y < sizeY; y++)
{
for (int z = 0; z < sizeZ; z++)
{
if (Helpers.IsBetweenInclusive(x, startSkipX, endSkipX))
{
if (Helpers.IsBetweenInclusive(y, startSkipY, endSkipY))
{
if (Helpers.IsBetweenInclusive(z, startSkipZ, endSkipZ))
{
continue;
}
}
}
sectors[x, y, z].Simulate(delta);
}
}
}
}
} }

View file

@ -1,4 +1,71 @@
using Godot;
public abstract class GameObject public abstract class GameObject
{ {
public abstract void Simulate(double delta); public Sector CurrentSector { get; private set; }
public Vector3 LocalCoordinates;
public Vector3Dec GlobalCoordinates;
public GameObject(Sector sector, Vector3 localCoordinates)
{
CurrentSector = sector;
LocalCoordinates = localCoordinates;
GlobalCoordinates = new
(
sector.GlobalStartCoordinates.X + (decimal)localCoordinates.X,
sector.GlobalStartCoordinates.Y + (decimal)localCoordinates.Y,
sector.GlobalStartCoordinates.Z + (decimal)localCoordinates.Z
);
}
public GameObject(Sector sector, double localX, double localY, double localZ)
{
CurrentSector = sector;
LocalCoordinates = new(localX, localY, localZ);
GlobalCoordinates = new
(
sector.GlobalStartCoordinates.X + (decimal)localX,
sector.GlobalStartCoordinates.Y + (decimal)localY,
sector.GlobalStartCoordinates.Z + (decimal)localZ
);
}
public GameObject(Vector3Dec coordinates)
{
GlobalCoordinates = coordinates;
UpdateSector();
}
public GameObject(decimal x, decimal y, decimal z)
{
GlobalCoordinates = new(x, y, z);
UpdateSector();
}
public Vector3 GetSectorCoordinates()
{
Vector3Dec relativeSectorCoordinates = GlobalCoordinates - CurrentSector.GlobalStartCoordinates;
return relativeSectorCoordinates.ToVector3();
}
public bool IsInSector()
{
return Helpers.IsInsideArea(CurrentSector.Size, GetSectorCoordinates());
}
public void UpdateSector()
{
}
public virtual void Simulate(double delta)
{
if (!IsInSector())
{
UpdateSector();
}
}
} }

View file

@ -0,0 +1,5 @@
using Godot;
public partial class StarNode : Node
{
}

View file

@ -0,0 +1 @@
uid://cbt6p1bhh4o8q

View file

@ -3,7 +3,7 @@ using Godot;
public partial class VesselNode : RigidBody3D public partial class VesselNode : RigidBody3D
{ {
public Vessel VesselData = new(); public Vessel VesselData;
public List<VesselModuleNode> Modules = []; public List<VesselModuleNode> Modules = [];
public override void _Ready() public override void _Ready()

View file

@ -0,0 +1,9 @@
using Godot;
public class Star(Sector sector, Vector3 localCoordinates) : GameObject(sector, localCoordinates)
{
public override void Simulate(double delta)
{
base.Simulate(delta);
}
}

View file

@ -0,0 +1 @@
uid://b8lixjwfffn6o

View file

@ -1,14 +1,17 @@
using System.Collections.Generic; using System.Collections.Generic;
using Godot;
public class Vessel : GameObject public class Vessel(Sector sector, Vector3 localCoordinates) : GameObject(sector, localCoordinates)
{ {
public List<VesselModule> Modules = []; public List<VesselModule> Modules = [];
public override void Simulate(double delta) public override void Simulate(double delta)
{ {
Modules.ForEach(module => base.Simulate(delta);
{
module.Simulate(delta); Modules.ForEach(module =>
}); {
} module.Simulate(delta);
});
}
} }

View file

@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using Godot;
public interface IGenerator
{
public Vector3I GetUniverseSize();
public Vector3 GetSectorSizeEachDirection();
public Task GenerateUniverse(Action<Universe> callback);
public Sector GenerateSector(Vector3I coordinates);
public Star GenerateStar(Sector sector, Vector3 localCoordinates);
public Star GenerateStar(Sector sector);
public Vessel GenerateShip(Sector sector, Vector3 localCoordinates);
public Vessel GenerateShip(Sector sector);
public Vessel GenerateStation(Sector sector, Vector3 localCoordinates);
public Vessel GenerateStation(Sector sector);
}

View file

@ -0,0 +1 @@
uid://cs64p311h0t20

View file

@ -0,0 +1,134 @@
using System;
using System.Threading.Tasks;
using Godot;
public class TestGenerator : IGenerator
{
public static Vector3I UNIVERSE_SIZE = new(3, 3, 3);
public static Vector3 SECTOR_SIZE_EACH_DIRECTION = new(1000, 1000, 1000);
public static int STARS_PER_SECTOR = 5;
public static int STARS_PER_SECTOR_VARIANCE = 2;
public static int SHIPS_PER_SECTOR = 0;
public static int SHIPS_PER_SECTOR_VARIANCE = 0;
public static int STATIONS_PER_SECTOR = 0;
public static int STATIONS_PER_SECTOR_VARIANCE = 0;
private RandomNumberGenerator rng;
public Vector3I GetUniverseSize()
{
return UNIVERSE_SIZE;
}
public Vector3 GetSectorSizeEachDirection()
{
return SECTOR_SIZE_EACH_DIRECTION;
}
public async Task GenerateUniverse(Action<Universe> callback)
{
rng = new();
Universe universe = new(UNIVERSE_SIZE);
for (int x = 0; x < UNIVERSE_SIZE.X; x++)
{
for (int y = 0; y < UNIVERSE_SIZE.Y; y++)
{
for (int z = 0; z < UNIVERSE_SIZE.Z; z++)
{
universe.Sectors[x, y, z] = GenerateSector(new(x, y, z));
}
}
}
callback(universe);
}
public Sector GenerateSector(Vector3I coordinates)
{
Sector sector = new(coordinates, SECTOR_SIZE_EACH_DIRECTION);
int starCount = rng.RandiRange(
STARS_PER_SECTOR - STARS_PER_SECTOR_VARIANCE,
STARS_PER_SECTOR + STARS_PER_SECTOR_VARIANCE
);
starCount = Mathf.Clamp(starCount, 0, int.MaxValue);
for (int i = 0; i < starCount; i++)
{
Vector3 localCoordinates = GenerateLocalCoordinates();
Star star = GenerateStar(sector, localCoordinates);
sector.GameObjects.Add(star);
}
int shipCount = rng.RandiRange(
SHIPS_PER_SECTOR - SHIPS_PER_SECTOR_VARIANCE,
SHIPS_PER_SECTOR + SHIPS_PER_SECTOR_VARIANCE
);
shipCount = Mathf.Clamp(shipCount, 0, int.MaxValue);
for (int i = 0; i < shipCount; i++)
{
Vector3 localCoordinates = GenerateLocalCoordinates();
Vessel ship = GenerateShip(sector, localCoordinates);
sector.GameObjects.Add(ship);
}
int stationCount = rng.RandiRange(
STATIONS_PER_SECTOR - STATIONS_PER_SECTOR_VARIANCE,
STATIONS_PER_SECTOR + STATIONS_PER_SECTOR_VARIANCE
);
stationCount = Mathf.Clamp(stationCount, 0, int.MaxValue);
for (int i = 0; i < stationCount; i++)
{
Vector3 localCoordinates = GenerateLocalCoordinates();
Vessel station = GenerateStation(sector, localCoordinates);
sector.GameObjects.Add(station);
}
return sector;
}
public Star GenerateStar(Sector sector, Vector3 localCoordinates)
{
return new Star(sector, localCoordinates);
}
public Star GenerateStar(Sector sector)
{
return GenerateStar(sector, GenerateLocalCoordinates());
}
public Vessel GenerateShip(Sector sector, Vector3 localCoordinates)
{
return new Vessel(sector, localCoordinates);
}
public Vessel GenerateShip(Sector sector)
{
return GenerateShip(sector, GenerateLocalCoordinates());
}
public Vessel GenerateStation(Sector sector, Vector3 localCoordinates)
{
return new Vessel(sector, localCoordinates);
}
public Vessel GenerateStation(Sector sector)
{
return GenerateStation(sector, GenerateLocalCoordinates());
}
public Vector3 GenerateLocalCoordinates()
{
double x = rng.Randf() * SECTOR_SIZE_EACH_DIRECTION.X;
double y = rng.Randf() * SECTOR_SIZE_EACH_DIRECTION.Y;
double z = rng.Randf() * SECTOR_SIZE_EACH_DIRECTION.Z;
return new(x, y, z);
}
}

View file

@ -0,0 +1 @@
uid://w7kt3hltqdo5

View file

@ -1,3 +1,24 @@
public static partial class Helpers using Godot;
public static class Helpers
{ {
public static bool IsBetweenInclusive(int value, int min, int max)
{
return value >= min && value <= max;
}
public static bool IsInsideArea(Vector3 area, Vector3 coordinates)
{
if (coordinates.X >= area.X && coordinates.Y >= area.Y && coordinates.Z >= area.Z)
{
return false;
}
if (coordinates.X < -area.X && coordinates.Y < -area.Y && coordinates.Z < -area.Z)
{
return false;
}
return true;
}
} }

37
scripts/Sector.cs Normal file
View file

@ -0,0 +1,37 @@
using System.Threading;
using Godot;
public class Sector
{
public Vector3I Coordinates;
public Vector3Dec GlobalStartCoordinates;
public Vector3Dec GlobalEndCoordinates;
public Vector3 Size;
public FastUniqueList<GameObject> GameObjects = new();
public Sector(Vector3I coordinates, Vector3 size)
{
Coordinates = coordinates;
decimal startX = Coordinates.X * (decimal)size.X;
decimal startY = Coordinates.Y * (decimal)size.Y;
decimal startZ = Coordinates.Z * (decimal)size.Z;
GlobalStartCoordinates = new(startX, startY, startZ);
Size = size;
}
public Sector(int x, int y, int z, Vector3 size) : this(new(x, y, z), size) { }
public void Simulate(double delta)
{
//GD.Print(Thread.CurrentThread.ManagedThreadId);
GameObjects.ForEach(gameObject =>
{
gameObject.Simulate(delta);
});
}
}

1
scripts/Sector.cs.uid Normal file
View file

@ -0,0 +1 @@
uid://bsnhay268qnfj

26
scripts/Universe.cs Normal file
View file

@ -0,0 +1,26 @@
using Godot;
public class Universe
{
public Sector[,,] Sectors;
public Universe(Vector3I size)
{
Sectors = new Sector[size.X, size.Y, size.Z];
}
public Universe(int sizeX, int sizeY, int sizeZ)
{
Sectors = new Sector[sizeX, sizeY, sizeZ];
}
public Sector GetSector(Vector3I coordinates)
{
return Sectors[coordinates.X, coordinates.Y, coordinates.Z];
}
public Sector GetSector(int x, int y, int z)
{
return Sectors[x, y, z];
}
}

1
scripts/Universe.cs.uid Normal file
View file

@ -0,0 +1 @@
uid://cqoe073upoupk

101
scripts/Vector3Dec.cs Normal file
View file

@ -0,0 +1,101 @@
using System;
using Godot;
public class Vector3Dec(decimal x, decimal y, decimal z)
{
public static Vector3Dec Zero { get; } = new(0, 0, 0);
public decimal X { get; } = x;
public decimal Y { get; } = y;
public decimal Z { get; } = z;
public Vector3 ToVector3()
{
return new((double)X, (double)Y, (double)Z);
}
public static Vector3Dec operator +(Vector3Dec vec1, Vector3Dec vec2)
{
return new(vec1.X + vec2.X, vec1.Y + vec2.Y, vec1.Z + vec2.Z);
}
public static Vector3Dec operator -(Vector3Dec vec1, Vector3Dec vec2)
{
return new(vec1.X - vec2.X, vec1.Y - vec2.Y, vec1.Z - vec2.Z);
}
public static Vector3Dec operator *(Vector3Dec vec1, Vector3Dec vec2)
{
return new(vec1.X * vec2.X, vec1.Y * vec2.Y, vec1.Z * vec2.Z);
}
public static Vector3Dec operator /(Vector3Dec vec1, Vector3Dec vec2)
{
return new(vec1.X / vec2.X, vec1.Y / vec2.Y, vec1.Z / vec2.Z);
}
public static Vector3Dec operator +(Vector3Dec vec, decimal value)
{
return new(vec.X + value, vec.Y + value, vec.Z + value);
}
public static Vector3Dec operator +(decimal value, Vector3Dec vec)
{
return vec + value;
}
public static Vector3Dec operator -(Vector3Dec vec, decimal value)
{
return new(vec.X - value, vec.Y - value, vec.Z - value);
}
public static Vector3Dec operator -(decimal value, Vector3Dec vec)
{
return vec - value;
}
public static Vector3Dec operator *(Vector3Dec vec, decimal value)
{
return new(vec.X * value, vec.Y * value, vec.Z * value);
}
public static Vector3Dec operator *(decimal value, Vector3Dec vec)
{
return vec * value;
}
public static Vector3Dec operator /(Vector3Dec vec, decimal value)
{
return new(vec.X / value, vec.Y / value, vec.Z / value);
}
public static Vector3Dec operator /(decimal value, Vector3Dec vec)
{
return vec / value;
}
public static Vector3Dec operator -(Vector3Dec vec)
{
return new(-vec.X, -vec.Y, -vec.Z);
}
public static bool operator ==(Vector3Dec vec1, Vector3Dec vec2)
{
return vec1.X == vec2.X && vec1.Y == vec2.Y && vec1.Z == vec2.Z;
}
public static bool operator !=(Vector3Dec vec1, Vector3Dec vec2)
{
return !(vec1 == vec2);
}
public override bool Equals(object obj)
{
return obj != null && obj is Vector3Dec vec && this == vec;
}
public override int GetHashCode()
{
return HashCode.Combine(X, Y, Z);
}
}

View file

@ -0,0 +1 @@
uid://def7cpbdn6gjm

View file

@ -2,10 +2,10 @@ using Godot;
public partial class VesselModuleNode : StaticBody3D public partial class VesselModuleNode : StaticBody3D
{ {
public VesselModule ModuleData { get; private set; } public VesselModule ModuleData { get; private set; }
public Node3D GetAnchor(string name) public Node3D GetAnchor(string name)
{ {
return GetNode<Node3D>(name); return GetNode<Node3D>(name);
} }
} }