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 _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(); 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(); 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 GetUnvisitedNeighbors( Cell cell ) { var neighbors = new List(); 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(); var finalIndices = new List(); 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( 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 }; } /// /// Создание матрицы трансформации /// private Matrix CreateTransform( Vector3 position, Rotation rotation, Vector3 scale ) { return Matrix.CreateScale( scale ) * Matrix.CreateRotation( rotation ) * Matrix.CreateTranslation( position ); } /// /// Оптимизированное построение стен лабиринта (единый меш) /// private void BuildMazeOptimized() { var builder = Model.Builder; // Создаем коллектор вершин для коллайдера var collisionVertices = new List(); 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(); renderer.Model = model; renderer.RenderType = ModelRenderer.ShadowRenderType.Off; if ( WallMaterial.IsValid() ) renderer.MaterialOverride = WallMaterial; var collider = _mazeWallsObject.Components.Create(); collider.Static = true; // var navMesh = _mazeWallsObject.Components.Create(); // 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(); 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(); var renderer = wall.Components.Create(); var navMeshArea = wall.Components.Create(); 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; } } }