Cuando recién comencé a programar, creo que, como muchos, quería hacer juegos. Pero antes de mí había muchos problemas arquitectónicos que no sabía cómo resolver, ni siquiera escuché sobre el doble buffer, y quería obtener el resultado lo antes posible. Por lo tanto, recientemente decidí escribir un proyecto en el que sería posible escribir juegos simples sin ningún problema. Los juegos en este proyecto se pueden crear como GameBoy, es decir: tetris, serpiente, etc. Pero también puede hacer clic con el mouse.
Enlace al proyecto en GitHub .
En este artículo quiero distinguir la creación de una serpiente.
Lo primero que debe comenzar es crear su propia clase de juego y heredar de la clase base del juego.
class Snake : Game
ya implementa el campo de juego y los eventos que ocurren cuando el juego pasa de un estado a otro. Esencialmente, todo lo que tenemos que hacer es declarar el manejo de eventos.
public Snake() : base() { OnPreview += BasePreview; OnNewGame += Snake_OnNewGame; OnUpdateGame += Snake_OnUpdateGame; OnGameOver += DrawScore; }
Para los eventos OnPreview y OnGameOver, ya hay apéndices ya preparados en la clase Juego, no puede implementarlos. Solo resta inicializar un nuevo juego y procesar eventos de actualización.
private GameBlock head; private List<GameBlock> body; private GameBlock eat; private void Snake_OnNewGame() { head = new GameBlock() { X = 10, Y = 10, Vector = Vector.Up, Color = GameColor.Green }; body = new List<GameBlock>(); body.Add( head ); body.Add( new GameBlock() { X = 10, Y = 11, Vector = Vector.Up, Color = GameColor.Black } ); body.Add( new GameBlock() { X = 10, Y = 12, Vector = Vector.Up, Color = GameColor.Black } ); CreateEat(); DrawField(); }
Puede trabajar con él directamente para dibujar un campo, o puede usar la clase GameBlock ya preparada, implementa cosas como la posición, la dirección del movimiento y el color.
En esta función, declaramos el cuerpo de la serpiente, creamos el primer alimento y mostramos lo que está sucediendo en el campo.
private void CreateEat() { var emptyBlocks = new List<GameBlock>(); for( int i = 0; i < MainForm.FIELD_SIZE; i++ ) for( int j = 0; j < MainForm.FIELD_SIZE; j++ ) if( CheckEmptyBlock( i, j ) ) emptyBlocks.Add(new GameBlock() { X = i, Y = j, Color = GameColor.Red } ); if (emptyBlocks.Count > 0) eat = emptyBlocks[random.Next( emptyBlocks.Count )]; }
Para crear una comida, obtenemos una lista de bloques vacíos y con la ayuda de un aleatorizador (que ya está declarado en el Juego) seleccionamos al azar. En caso de que la serpiente haya ocupado todo el campo, se verifica el tamaño de la lista.
En realidad, la función de verificar la celda vacía:
private bool CheckEmptyBlock(int x, int y) => !( x < 0 || y < 0 || x == MainForm.FIELD_SIZE || y == MainForm.FIELD_SIZE ) && !body.Exists( a => a.Equals( new GameBlock() { X = x, Y = y } ) );
La representación del campo es la siguiente:
private void DrawField() { Field.Clear( GameColor.White ); Field.DrawGameBlock( eat ); Field.DrawGameBlocks( body ); WriteScore(); }
Como no es difícil de adivinar, el campo se borra en blanco y se muestra la comida con una serpiente. WriteScore es otra característica estándar para mostrar una puntuación en una barra de estado especial.
Entonces, pasamos al evento de actualización del juego, que ocurre con una frecuencia de 300 ms.
private void Snake_OnUpdateGame( Controller controller ) { ControlMove( controller.GameKey ); if( CheckGameOver() ) GameOver(); else SnakeMove(); }
Suceden cuatro cosas: cambiar la dirección del movimiento, verificar el final del juego, llamar al evento del final del juego y mover la serpiente en caso de que todo esté en orden.
private void ControlMove( GameKey key ) { switch( key ) { case GameKey.Left: head.Vector = head.Vector == Vector.Right ? Vector.Right : Vector.Left; break; case GameKey.Right: head.Vector = head.Vector == Vector.Left ? Vector.Left : Vector.Right; break; case GameKey.Up: head.Vector = head.Vector == Vector.Down ? Vector.Down : Vector.Up; break; case GameKey.Down: head.Vector = head.Vector == Vector.Up ? Vector.Up : Vector.Down; break; default: break; } }
Para cambiar la dirección del movimiento en la serpiente, necesitamos cambiar el vector en su cabeza. Por lo tanto, en el control del movimiento hay un control para el caso de inversión del vector, para que la serpiente no comience a trepar sobre sí misma.
private bool CheckGameOver() { switch( head.Vector ) { case Vector.Up: return !CheckEmptyBlock( head.X, head.Y - 1 ); case Vector.Down: return !CheckEmptyBlock( head.X, head.Y + 1 ); case Vector.Left: return !CheckEmptyBlock( head.X - 1, head.Y ); case Vector.Right: return !CheckEmptyBlock( head.X + 1, head.Y ); default: throw new NotImplementedException(); } }
Para verificar el final del juego, es suficiente verificar si el bloque en la dirección es libre o no. Como puede suponer, la comida en el cheque se ignora.
Queda por analizar la función de movimiento de la serpiente:
private void SnakeMove() { var temp = body.Last().Copy(); foreach( var block in body ) block.Move(); for( int i = body.Count - 1; i > 0; i-- ) body[i].Vector = body[i - 1].Vector; if( head.Equals( eat ) ) { score++; body.Add( temp ); CreateEat(); } DrawField(); }
El final de la cola se copia de modo que si se alcanza la comida, agréguela como una extensión de serpiente. Mover bloques no es difícil, porque esta función ya está implementada en la clase de bloque. Luego, los vectores se distribuyen sobre el movimiento de la serpiente y se verifica la intersección con los alimentos. Si se encuentran alimentos, la factura aumenta, la serpiente crece y se crean nuevos alimentos. Para que nuestro juego aparezca en la lista de juegos, debe agregarlo a la inicialización del formulario:
List<Game> games = new List<Game>(); games.Add( new Snake() ); games.Add( new Tetris() ); games.Add( new Life() ); Application.Run( new MainForm( games ) );
Eso es todo El código completo del juego solo tomó 102 líneas. Como puede ver en el ejemplo, los tetris y la vida del juego ya se han agregado al proyecto. A continuación puede encontrar el resultado.
Menú de selección de juego
Proceso del juego
El final del juego