sasalka/Code/Maze/Maze.cs
2025-06-08 23:47:32 +03:00

552 lines
15 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

public sealed class Maze : Component
{
[Property, Sync] public int Width { get; set; } = 10;
[Property, Sync] public int Height { get; set; } = 10;
[Property, Sync] public float WallHeight { get; set; } = 5f;
[Property, Sync] public float WallThickness { get; set; } = 5f;
[Property, Sync] public float CellSize { get; set; } = 200f;
[Property, Sync] public float PassThrougthPercent { get; set; } = 0.1f;
[Property] public int CenterClearRadius { get; set; } = 1;
[Property] public GameObject EnemyPrefab { get; set; }
private GameObject _mazeWallsObject;
public Cell[,] Cells;
private List<GameObject> _walls = new();
[Property] public GameObject Floor { get; set; }
[Property] public bool IsReady { get; set; } = false;
[Property] public Material WallMaterial { get; set; } // Добавлено свойство для материала
private static readonly string WallModelPath = "models/dev/box.vmdl_c";
private Vector3 mazeOffset =>
new Vector3( -(Width * CellSize) / 2 + CellSize / 2, -(Height * CellSize) / 2 + CellSize / 2, 0 );
[Property, Sync( SyncFlags.FromHost ), Change( "OnSeedChanged" )]
public int MazeSeed { get; set; } = -1;
void OnSeedChanged( int oldValue, int newValue )
{
if ( newValue != oldValue )
{
IsReady = false;
CreateMaze();
}
}
[Rpc.Broadcast]
public void RpcRequestMaze()
{
if ( Networking.IsHost )
{
MazeSeed = Game.Random.Int( 0, 999999999 );
}
}
public async void CreateMaze()
{
Log.Info( $"CREATE MAZE {MazeSeed}" );
ScaleFloor();
ClearMaze();
GenerateMaze();
await Task.Delay( 10 );
BuildMazeOptimized();
await Task.Delay( 10 );
SpawnEnemy();
IsReady = true;
}
private void RemoveCenterWalls()
{
if ( Cells == null )
{
Log.Warning( "RemoveCenterWalls: _cells is null" );
return;
}
int centerX = Width / 2;
int centerY = Height / 2;
int startX = Math.Max( 0, centerX - CenterClearRadius );
int endX = Math.Min( Width - 1, centerX + CenterClearRadius );
int startY = Math.Max( 0, centerY - CenterClearRadius );
int endY = Math.Min( Height - 1, centerY + CenterClearRadius );
for ( int x = startX; x <= endX; x++ )
{
for ( int y = startY; y <= endY; y++ )
{
if ( Cells[x, y] == null ) continue;
var cell = Cells[x, y];
for ( int i = 0; i < 4; i++ )
{
if ( i >= 0 && i < cell.Walls.Length )
cell.Walls[i] = false;
}
// Сброс стен у соседей
if ( x > 0 && Cells[x - 1, y] != null )
Cells[x - 1, y].Walls[1] = false; // Left neighbor's Right
if ( x < Width - 1 && Cells[x + 1, y] != null )
Cells[x + 1, y].Walls[3] = false; // Right neighbor's Left
if ( y > 0 && Cells[x, y - 1] != null )
Cells[x, y - 1].Walls[2] = false; // Bottom neighbor's Top
if ( y < Height - 1 && Cells[x, y + 1] != null )
Cells[x, y + 1].Walls[0] = false; // Top neighbor's Bottom
}
}
}
public Vector3 GetRandomCellPosition()
{
var randomCell = Cells[Game.Random.Int( 0, Width - 1 ), Game.Random.Int( 0, Height - 1 )];
return WorldPosition + new Vector3(
randomCell.X * CellSize,
randomCell.Y * CellSize,
0
) + mazeOffset;
}
private void SpawnEnemy()
{
if ( !Networking.IsHost ) return;
var enemy = EnemyPrefab.Clone();
enemy.WorldPosition = GetRandomCellPosition();
enemy.NetworkSpawn( null );
var agent = enemy.Components.Get<NavMeshAgent>();
if ( agent != null )
{
agent.Enabled = false;
agent.WorldPosition = enemy.WorldPosition;
agent.Enabled = true;
}
}
private void ClearMaze()
{
// Удаляем оптимизированные стены
if ( _mazeWallsObject.IsValid() )
{
_mazeWallsObject.Destroy();
_mazeWallsObject = null;
}
// Удаляем старые стены (для обратной совместимости)
foreach ( var wall in _walls )
{
if ( wall.IsValid ) wall.Destroy();
}
_walls.Clear();
}
private void GenerateMaze()
{
Cells = new Cell[Width, Height];
for ( int x = 0; x < Width; x++ )
{
for ( int y = 0; y < Height; y++ )
{
Cells[x, y] = new Cell( x, y );
}
}
var stack = new Stack<Cell>();
var rng = new Random( MazeSeed );
var startCell = Cells[0, 0];
startCell.Visited = true;
stack.Push( startCell );
while ( stack.Count > 0 )
{
var current = stack.Peek();
var neighbors = GetUnvisitedNeighbors( current );
if ( neighbors.Count > 0 )
{
var next = neighbors[rng.Next( neighbors.Count )];
RemoveWall( current, next );
next.Visited = true;
stack.Push( next );
}
else
{
stack.Pop();
}
}
AddExtraPassages( count: (int)(Width * Height * PassThrougthPercent) ); // 10% от клеток
RemoveCenterWalls();
}
private List<Cell> GetUnvisitedNeighbors( Cell cell )
{
var neighbors = new List<Cell>();
int x = cell.X;
int y = cell.Y;
if ( x > 0 && !Cells[x - 1, y].Visited ) neighbors.Add( Cells[x - 1, y] );
if ( x < Width - 1 && !Cells[x + 1, y].Visited ) neighbors.Add( Cells[x + 1, y] );
if ( y > 0 && !Cells[x, y - 1].Visited ) neighbors.Add( Cells[x, y - 1] );
if ( y < Height - 1 && !Cells[x, y + 1].Visited ) neighbors.Add( Cells[x, y + 1] );
return neighbors;
}
private void RemoveWall( Cell current, Cell next )
{
int dx = next.X - current.X;
int dy = next.Y - current.Y;
if ( dx == 1 )
{
current.Walls[1] = false; // Right
next.Walls[3] = false; // Left
}
else if ( dx == -1 )
{
current.Walls[3] = false; // Left
next.Walls[1] = false; // Right
}
else if ( dy == 1 )
{
current.Walls[2] = false; // Top
next.Walls[0] = false; // Bottom
}
else if ( dy == -1 )
{
current.Walls[0] = false; // Bottom
next.Walls[2] = false; // Top
}
}
private void BuildMaze()
{
for ( int x = 0; x < Width; x++ )
{
for ( int y = 0; y < Height; y++ )
{
var cell = Cells[x, y];
Vector3 cellCenter = new Vector3( x * CellSize, y * CellSize, 0 );
if ( cell.Walls[0] ) // Bottom
{
Vector3 pos = cellCenter + new Vector3( 0, -CellSize / 2, 0 );
SpawnWall( pos, Rotation.FromYaw( 0 ) );
}
if ( cell.Walls[1] ) // Right
{
Vector3 pos = cellCenter + new Vector3( CellSize / 2, 0, 0 );
SpawnWall( pos, Rotation.FromYaw( 90 ) );
}
if ( cell.Walls[2] ) // Top
{
Vector3 pos = cellCenter + new Vector3( 0, CellSize / 2, 0 );
SpawnWall( pos, Rotation.FromYaw( 0 ) );
}
if ( cell.Walls[3] ) // Left
{
Vector3 pos = cellCenter + new Vector3( -CellSize / 2, 0, 0 );
SpawnWall( pos, Rotation.FromYaw( 90 ) );
}
}
}
}
private void AddBox( ModelBuilder builder, Matrix transform, Vector3 size )
{
Vector3 halfSize = size / 2;
Vector3[] localVerts = new Vector3[]
{
new(-halfSize.x, -halfSize.y, -halfSize.z), // 0
new(halfSize.x, -halfSize.y, -halfSize.z), // 1
new(halfSize.x, halfSize.y, -halfSize.z), // 2
new(-halfSize.x, halfSize.y, -halfSize.z), // 3
new(-halfSize.x, -halfSize.y, halfSize.z), // 4
new(halfSize.x, -halfSize.y, halfSize.z), // 5
new(halfSize.x, halfSize.y, halfSize.z), // 6
new(-halfSize.x, halfSize.y, halfSize.z) // 7
};
int[] indices = new int[]
{
0, 2, 1, 0, 3, 2, // bottom (задом наперёд)
4, 5, 6, 4, 6, 7, // top
0, 5, 4, 0, 1, 5, // front
1, 6, 5, 1, 2, 6, // right
2, 7, 6, 2, 3, 7, // back
3, 4, 7, 3, 0, 4 // left
};
Vector3[] faceNormals = new Vector3[]
{
Vector3.Down, // bottom
Vector3.Up, // top
Vector3.Backward, // front
Vector3.Right, // right
Vector3.Forward, // back
Vector3.Left // left
};
var vertices = new List<Vertex>();
var finalIndices = new List<int>();
for ( int i = 0; i < indices.Length; i += 6 )
{
Vector3 localNormal = faceNormals[i / 6];
Vector3 worldNormal = transform.TransformNormal( localNormal );
for ( int j = 0; j < 6; j++ )
{
int vertexIndex = indices[i + j];
Vector3 localPos = localVerts[vertexIndex];
Vector3 worldPos = transform.Transform( localPos );
// Vector4 texCoord = new Vector4( 0, 0, 0, 0 ); // упрощённые UV
Vector4 texCoord = new Vector4( GetUVForVertex( j ).x, GetUVForVertex( j ).y, 0, 0 );
Vertex vertex = new Vertex(
position: worldPos,
normal: worldNormal,
tangent: Vector3.Right,
texCoord0: texCoord
);
vertex.Color = Color32.White;
vertex.TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertices.Add( vertex );
finalIndices.Add( vertices.Count - 1 );
}
}
if ( WallMaterial == null )
{
Log.Error( "WallMaterial not set!" );
return;
}
var mesh = new Mesh( WallMaterial, MeshPrimitiveType.Triangles );
mesh.CreateVertexBuffer<Vertex>( vertices.Count, Vertex.Layout, vertices );
mesh.CreateIndexBuffer( finalIndices.Count, finalIndices );
builder.AddMesh( mesh ).AddCollisionMesh( vertices.Select( x => x.Position ).ToList(), indices.ToList() );
}
private Vector2 GetUVForVertex( int index )
{
return (index % 4) switch
{
0 => new Vector2( 0, 0 ),
1 => new Vector2( 1, 0 ),
2 => new Vector2( 1, 1 ),
3 => new Vector2( 0, 1 ),
_ => Vector2.Zero
};
}
/// <summary>
/// Создание матрицы трансформации
/// </summary>
private Matrix CreateTransform( Vector3 position, Rotation rotation, Vector3 scale )
{
return Matrix.CreateScale( scale )
* Matrix.CreateRotation( rotation )
* Matrix.CreateTranslation( position );
}
/// <summary>
/// Оптимизированное построение стен лабиринта (единый меш)
/// </summary>
private void BuildMazeOptimized()
{
var builder = Model.Builder;
// Создаем коллектор вершин для коллайдера
var collisionVertices = new List<Vector3>();
for ( int x = 0; x < Width; x++ )
{
for ( int y = 0; y < Height; y++ )
{
var cell = Cells[x, y];
Vector3 cellCenter =
new Vector3( x * CellSize, y * CellSize, 0 ) +
mazeOffset; // + new Vector3( 0, 0, WallHeight * 24);
void AddWall( Vector3 position, Rotation rotation, Vector3 size )
{
var transform = Matrix.CreateScale( size )
* Matrix.CreateRotation( rotation )
* Matrix.CreateTranslation( position );
// Добавляем меш к модели (используй свой AddBox)
AddBox( builder, transform, new Vector3( 1, 1, WallHeight * 24 ) );
// Добавляем коллизию — формируем коллизионный hull из углов куба
// Куб - 8 вершин (как в твоём AddBox), трансформируем и добавляем
Vector3 half = new Vector3( 0.5f, 0.5f, 0.5f );
Vector3[] localVerts = new Vector3[]
{
new Vector3( -half.x, -half.y, -half.z ), new Vector3( half.x, -half.y, -half.z ),
new Vector3( half.x, half.y, -half.z ), new Vector3( -half.x, half.y, -half.z ),
new Vector3( -half.x, -half.y, half.z ), new Vector3( half.x, -half.y, half.z ),
new Vector3( half.x, half.y, half.z ), new Vector3( -half.x, half.y, half.z ),
};
foreach ( var v in localVerts )
collisionVertices.Add( transform.Transform( v ) );
}
if ( cell.Walls[0] )
AddWall( cellCenter + new Vector3( 0, -CellSize / 2, 0 ), Rotation.Identity,
new Vector3( CellSize, WallThickness, WallHeight ) );
if ( cell.Walls[1] )
AddWall( cellCenter + new Vector3( CellSize / 2, 0, 0 ), Rotation.FromYaw( 90 ),
new Vector3( CellSize, WallThickness, WallHeight ) );
if ( cell.Walls[2] )
AddWall( cellCenter + new Vector3( 0, CellSize / 2, 0 ), Rotation.Identity,
new Vector3( CellSize, WallThickness, WallHeight ) );
if ( cell.Walls[3] )
AddWall( cellCenter + new Vector3( -CellSize / 2, 0, 0 ), Rotation.FromYaw( 90 ),
new Vector3( CellSize, WallThickness, WallHeight ) );
}
}
// Создаем модель с коллизией из всех добавленных hull вершин
var model = builder
.WithMass( 0 ) // Статическая модель
.Create();
if ( _mazeWallsObject.IsValid() )
_mazeWallsObject.Destroy();
_mazeWallsObject = new GameObject();
_mazeWallsObject.Name = "Maze Walls";
_mazeWallsObject.SetParent( GameObject );
_mazeWallsObject.LocalPosition = new Vector3( 0, 0, WallHeight * 24 );
var renderer = _mazeWallsObject.Components.Create<ModelRenderer>();
renderer.Model = model;
renderer.RenderType = ModelRenderer.ShadowRenderType.Off;
if ( WallMaterial.IsValid() )
renderer.MaterialOverride = WallMaterial;
var collider = _mazeWallsObject.Components.Create<ModelCollider>();
collider.Static = true;
// var navMesh = _mazeWallsObject.Components.Create<NavMeshArea>();
// navMesh.LinkedCollider = collider;
// navMesh.IsBlocker = true;
// _mazeWallsObject.NetworkSpawn( null );
Scene.NavMesh.SetDirty();
}
private void ScaleFloor()
{
Floor.LocalPosition = new Vector3( 0, 0, -WallHeight );
Floor.LocalScale = new Vector3( CellSize / 100 * Width, CellSize / 100 * Height, 0.5f );
var renderer = Floor.Components.Get<ModelRenderer>();
var material = renderer.MaterialOverride;
material?.Set( "g_vTexCoordScale", new Vector2( Width, Height ) );
Floor.NetworkSpawn( null );
}
private void SpawnWall( Vector3 position, Rotation rotation )
{
var wall = new GameObject();
wall.SetParent( GameObject );
wall.LocalPosition = position + mazeOffset + new Vector3( 0, 0, WallHeight * 24 );
wall.WorldRotation = rotation;
wall.LocalScale = new Vector3( CellSize / 50, WallThickness / 10, WallHeight );
var collider = wall.Components.Create<BoxCollider>();
var renderer = wall.Components.Create<ModelRenderer>();
var navMeshArea = wall.Components.Create<NavMeshArea>();
navMeshArea.IsBlocker = true;
navMeshArea.LinkedCollider = collider;
renderer.Model = Model.Load( WallModelPath );
collider.Static = true;
wall.NetworkSpawn( null );
_walls.Add( wall );
}
private void AddExtraPassages( int count )
{
var rng = new Random( MazeSeed );
int added = 0;
while ( added < count )
{
int x = rng.Next( 1, Width - 1 );
int y = rng.Next( 1, Height - 1 );
var cell = Cells[x, y];
// Выбери случайного соседа
List<(int dx, int dy, int dir)> directions = new()
{
(0, -1, 0), // Bottom
(1, 0, 1), // Right
(0, 1, 2), // Top
(-1, 0, 3) // Left
};
var (dx, dy, dir) = directions[rng.Next( directions.Count )];
int nx = x + dx;
int ny = y + dy;
if ( nx < 0 || nx >= Width || ny < 0 || ny >= Height )
continue;
var neighbor = Cells[nx, ny];
if ( cell.Walls[dir] )
{
RemoveWall( cell, neighbor );
added++;
}
}
}
public class Cell
{
public int X;
public int Y;
public bool Visited;
public bool[] Walls = { true, true, true, true }; // Bottom, Right, Top, Left
public Cell( int x, int y )
{
X = x;
Y = y;
}
}
}