552 lines
15 KiB
C#
552 lines
15 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|