Traducción del artículo original de Michalis Kamburelis.
Contenido
- 1 Introducción
- 2 Codificación del juego
- 3 Ejercicios
- 4 Hacer que el código reconozca «qué es una pieza de ajedrez» utilizando comportamientos
- 5 Seleccionar objetos 3D usando el mouse
- 6 Permitir al usuario elegir el ángulo y la fuerza para lanzar la pieza de ajedrez
- 7 ¡Lanza esa pieza de ajedrez!
- 8 Conclusión e ideas para el futuro
Introducción
Bienvenido a la segunda parte del artículo sobre cómo crear un juego sencillo de física en 3D utilizando Castle Game Engine.
Castle Game Engine es un motor de juego 3D y 2D multiplataforma (escritorio, móvil, consolas) que utiliza Pascal moderno. Es gratuito, de código abierto y compatible con FPC y Delphi.
En la primera parte, aprendimos a utilizar el editor visual y diseñamos un tablero de ajedrez con piezas de ajedrez. Luego utilizamos física para lanzar una pieza de ajedrez de manera que colisionara y derribara otras piezas. Recuerda que esta es una mala manera de jugar ajedrez, ¡pero es realmente divertido!
Si te perdiste la primera parte, aún puedes «saltar» en este punto. Simplemente descarga Castle Game Engine desde https://castle-engine.io/ y configura el tablero de ajedrez y las piezas de ajedrez tú mismo, o utiliza nuestro proyecto de ejemplo listo desde https://github.com/castle-engine/bad-chess/ en el subdirectorio project/version_1_designed_in_editor. Esta versión del proyecto es un buen punto de partida para esta parte del artículo.
Te animamos a seguir este artículo y realizar todos los pasos por ti mismo, para crear un juguete similar. Si te quedas atascado en algún momento, siempre puedes consultar el proyecto terminado. Está disponible en el subdirectorio project/version_2_with_code en el mismo repositorio, https://github.com/castle-engine/bad-chess/. Es el proyecto final, con todo lo descrito en este artículo hecho y funcionando.
Y si realmente solo quieres jugar la peor versión de ajedrez ahora mismo, puedes descargar el juego compilado listo (para Linux o Windows) desde https://castle-engine.itch.io/bad-chess. ¡Disfruta!
Codificación del juego
En esta parte nos enfocaremos en aprender cómo usar código Pascal para hacer que las cosas sucedan en tu juego.
El núcleo de Castle Game Engine son simplemente un conjunto de unidades Pascal que pueden compilarse utilizando FPC y Delphi. Esto significa que los juegos que creamos son programas Pascal regulares que utilizan algunas unidades específicas de Castle Game Engine. Esto te permite utilizar el flujo de trabajo que ya conoces y prefieres, con el editor de texto y el compilador Pascal que elijas.
En particular, soportamos Delphi, Lazarus, VS Code o cualquier otro editor personalizado (como Emacs). Tenemos una documentación dedicada con consejos específicos para cada IDE en https://castle-engine.io/manual_ide.php. Básicamente, abre en el editor de Castle Game Engine el panel «Preferences → Code Editor», configura allí el IDE Pascal que utilizas y todo debería funcionar sin problemas. Si haces doble clic en un archivo Pascal desde el editor de CGE, se abrirá en el editor de texto que configuraste.
Específicamente para los usuarios de VS Code, la página https://castle-engine.io/vscode contiene información sobre cómo configurar VS Code con el servidor LSP de Castle Game Engine para obtener una excelente finalización de código. Tenemos una extensión dedicada que proporciona una integración perfecta de Castle Game Engine con VS Code. Proporciona resaltado de sintaxis Pascal, autocompletado de código y la capacidad de compilar, ejecutar o depurar un proyecto de Castle Game Engine directamente desde VS Code. En este enlace puedes descargar los archivos necesarios https://castle-engine.io/vscode y más documentación.
Es importante destacar que, aunque el enfoque de este capítulo sea escribir código Pascal, no dejamos de utilizar el editor de Castle Game Engine. Hay varias cosas que puedes hacer en el editor para que el diseño sea «amigable» a la manipulación de código, y las exploraremos en este artículo. Por lo tanto, escribir código Pascal y editar el diseño visualmente van de la mano.
Ejercicios
Manejar una pulsación de tecla para cambiar la posición de un objeto.
Empecemos con algo sencillo. Nuestro primer objetivo: cuando el usuario presione la tecla x, queremos elevar un poco la pieza del rey negro de ajedrez. Es una prueba simple que podemos hacer:
- Reaccionar a la entrada del usuario (pulsación de tecla).
- En respuesta, hacer algo interesante en el mundo 3D (mover una pieza de ajedrez).
La mayoría del código que escribas en Castle Game Engine se coloca en una unidad asociada con una vista. Hablamos sobre qué es una vista en Castle Game Engine en la parte anterior del artículo; en resumen, utilizas vistas de manera similar a como usas formularios en una aplicación típica de Delphi FMX / VCL o Lazarus LCL: una vista es un diseño visual (en data/gameviewmain.castle-user-interface) y el código asociado (en code/gameviewmain.pas).
Así que abramos el archivo code/gameviewmain.pas en tu IDE Pascal favorito. En el editor de Castle Game Engine, simplemente usa el panel «Files» en la parte inferior. Entra en el subdirectorio code y haz doble clic en el archivo gameviewmain.pas. Alternativamente, puedes abrir tu IDE Pascal y desde allí abrir el proyecto Pascal. Los archivos básicos del proyecto (como my_project.dproj para Delphi o my_project.lpi para Lazarus) ya han sido generados para ti.
Mantén abierto también el editor visual de Castle Game Engine, con nuestro diseño visual en data/gameviewmain.castle-user-interface. Ajustaremos o consultaremos nuestro diseño visual ocasionalmente, para asegurarnos de que sea útil para nuestra lógica de código.
Para empezar, queremos saber el nombre del componente que representa al rey negro. Como has visto al diseñar formularios en Lazarus y Delphi, cada componente tiene un nombre que corresponde a cómo se puede acceder a ese componente desde el código. Puedes editar el nombre del componente en Castle Game Engine ya sea editando la fila «Name» en el «Object Inspector» (a la derecha) o editando el nombre en la jerarquía (a la izquierda). Simplemente haz clic en el nombre del componente en la jerarquía o presiona F2 para entrar en la edición de nombres. En la captura de pantalla a continuación, puedes ver que el rey negro está nombrado como SceneBlackKing1. Puedo usar Ctrl+C para copiar esto al portapapeles.
Claro, aquí tienes la traducción:
Ten en cuenta que, para este primer ejercicio de código, asumimos que la pieza de ajedrez (SceneBlackKing1) no tiene ningún componente de física. Si has añadido componentes TCastleRigidBody o TCastleXxxCollider como comportamientos de SceneBlackKing1, por favor elimínalos por ahora. Los restauraremos en el próximo ejercicio.
Ahora tenemos que declarar la variable con el mismo nombre exacto en la vista. Se inicializará automáticamente para apuntar al componente cuando iniciemos la vista. Haz esto en la sección «published» de la clase TViewMain. Este es cómo debería lucir el resultado final:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
uses Classes, CastleVectors, CastleComponentSerialize, CastleUIControls, CastleControls, CastleKeysMouse, CastleScene; type { Main view, where most of the application logic takes place. } TViewMain = class(TCastleView) published { Components designed using CGE editor. These fields will be automatically initialized at Start. } LabelFps: TCastleLabel; SceneBlackKing1: TCastleScene; //< new line public ... |
Nota: En este momento, el editor de Castle Game Engine no hace esto automáticamente por ti. Es decir, no actualizamos automáticamente tus fuentes Pascal para declarar todos los componentes. Tenemos planes de implementar esto pronto. La experiencia del usuario tendrá que ser un poco diferente a la de los formularios en Delphi y Lazarus, porque los diseños visuales de juegos pueden tener fácilmente cientos de componentes que no se supone que se utilicen desde el código, por lo que sincronizarlos todos con el código Pascal crearía ruido innecesario en tu unidad Pascal. En su lugar, haremos un botón para exponer solo un subconjunto de componentes diseñados para el código.
Una vez que hayas declarado el campo «published», podemos acceder a SceneBlackKing1 desde el código, obteniendo y estableciendo sus propiedades, llamando a sus métodos donde queramos. Para este ejercicio, modificaremos la propiedad Translation de nuestra pieza de ajedrez, que cambia la posición del objeto.
Es una propiedad de tipo TVector3. TVector3 es un registro avanzado en Castle Game Engine que representa un vector 3D, en este caso una posición, pero también lo usamos en muchos otros casos, por ejemplo, para representar una dirección o incluso un color RGB. Hay varias funciones útiles definidas para ayudarte a trabajar con TVector3, en particular:
- La función Vector3(…) devuelve un nuevo valor TVector3 con las coordenadas proporcionadas.
- Los operadores aritméticos como + funcionan con valores TVector3.
Esto significa que podemos mover el objeto fácilmente escribiendo un código como este:
1 |
SceneBlackKing1.Translation := SceneBlackKing1.Translation + Vector3(0, 1, 0); |
¿Dónde poner esta declaración? En general, puedes usar este código en cualquier lugar de tu vista (siempre que se ejecute después de que la vista se haya iniciado). En este caso, queremos reaccionar cuando el usuario presione la tecla x. Para lograr esto, podemos editar el método TViewMain.Press en la vista. La implementación vacía de este método ya está presente, con algunos comentarios útiles, así que simplemente podemos llenarlo con nuestro código:
1 2 3 4 5 6 7 8 9 10 11 |
function TViewMain.Press(const Event: TInputPressRelease): Boolean; begin Result := inherited; if Result then Exit; // allow the ancestor to handle keys if Event.IsKey(keyX) then begin SceneBlackKing1.Translation := SceneBlackKing1.Translation + Vector3(0, 1, 0); Exit(true); // key was handled end; end; |
Compila y ejecuta el juego (por ejemplo, presionando F9 en el editor de Castle Game Engine, o en Delphi, o en Lazarus) y presiona X para ver cómo funciona.
Empujar la pieza de ajedrez utilizando física
Vamos a empujar (lanzar, arrojar) una pieza de ajedrez utilizando física. Asegurémonos de que podemos usar código para este propósito en este ejercicio. La pieza de ajedrez que empujaremos y la dirección en la que la empujaremos estarán codificadas en este ejercicio, pero ganaremos confianza en nuestra capacidad para utilizar la física desde el código Pascal.
Utilizaremos nuevamente el rey negro.
Para hacer esto, asegúrate de agregar los componentes de física a la pieza de ajedrez relevante. Describimos cómo hacer esto en la primera parte del artículo. Un breve resumen: haz clic derecho en el componente (SceneBlackKing1 en este caso) y en el menú contextual elige «Agregar Comportamiento → Física → Collider → Caja (TCastleBoxCollider)». Asegúrate también de tener física activada en el tablero de ajedrez (con TCastleMeshCollider), de lo contrario, la pieza de ajedrez caería debido a la gravedad tan pronto como ejecutes el juego.
Debería tener un aspecto similar a este:
Para empujarlo usando física, queremos utilizar el método ApplyImpulse del componente TCastleRigidBody asociado a la pieza de ajedrez.
Puedes obtener el componente TCastleRigidBody utilizando el método SceneBlackKing1.FindBehavior(TCastleRigidBody), como se muestra a continuación.
Alternativamente, también podrías declarar y acceder a RigidBody1: TCastleRigidBody en la sección published de tu vista. No mostramos este enfoque aquí simplemente porque el uso de FindBehavior parece más educativo en este punto, es decir, encontrarás FindBehavior útil en más situaciones.
El método ApplyImpulse toma dos parámetros: la dirección del impulso (como TVector3; la longitud de este vector determina la fuerza del impulso) y la posición desde la cual proviene el impulso (es más simple usar simplemente la posición de la pieza de ajedrez aquí).
Al final, esta es la versión modificada de TViewMain.Press que debes utilizar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function TViewMain.Press(const Event: TInputPressRelease): Boolean; var MyBody: TCastleRigidBody; begin Result := inherited; if Result then Exit; // allow the ancestor to handle keys if Event.IsKey(keyX) then begin MyBody := SceneBlackKing1.FindBehavior(TCastleRigidBody) as TCastleRigidBody; MyBody.ApplyImpulse(Vector3(0, 10, 0), SceneBlackKing1.WorldTranslation); Exit(true); // key was handled end; end; |
Arriba usamos el vector de dirección Vector3(0, 10, 0), lo cual significa «hacia arriba, con fuerza 10». Puedes experimentar con diferentes direcciones y fuerzas. Si quisiéramos empujar la pieza de ajedrez horizontalmente, usaríamos una dirección con valores no nulos en X y/o Z, y dejaríamos el eje Y en cero.
En la cláusula uses, agrega también la unidad CastleTransform, para tener definida la clase TCastleRigidBody.
Como de costumbre, compila y ejecuta el juego para probarlo. Al presionar X ahora debería hacer que la pieza de ajedrez se eleve.
Puedes presionar X repetidamente, incluso cuando la pieza de ajedrez ya está en el aire. Como se puede ver en el código, no lo hemos asegurado, así que permitimos empujar un objeto que ya está volando. No cubriremos esto en este ejercicio, pero podrías usar MyBody.PhysicsRayCast para lanzar un rayo con dirección Vector3(0, -1, 0) y ver si la pieza de ajedrez ya está en el aire.
Hacer que el código reconozca «qué es una pieza de ajedrez» utilizando comportamientos
Para implementar nuestra lógica deseada, el código debe saber de alguna manera «qué es una pieza de ajedrez». Hasta ahora, nuestro mundo 3D es una colección de componentes TCastleScene, pero esto no nos proporciona suficiente información para distinguir entre piezas de ajedrez y otros objetos (como un tablero de ajedrez). ¡Queremos hacer algo loco, pero no queremos voltear el tablero de ajedrez! Al menos no esta vez 🙂
Para «marcar» que el componente TCastleScene dado es una pieza de ajedrez, inventaremos una nueva clase llamada TChessPieceBehavior que desciende de la clase TCastleBehavior. Luego adjuntaremos instancias de esta clase a los componentes TCastleScene que representan piezas de ajedrez. En el futuro, esta clase puede tener más campos (que contengan información específica de esta pieza de ajedrez) y métodos. Para empezar, la mera existencia de una instancia TCastleBehavior adjunta a una escena indica «esto es una pieza de ajedrez».
Para obtener más información sobre cómo funcionan nuestros comportamientos, consulta la documentación y ejemplos en https://castle-engine.io/behaviors. También puedes crear un nuevo proyecto a partir de la plantilla «Juego FPS 3D» y ver cómo se define y utiliza la clase TEnemy (descendiente de TCastleBehavior). Los comportamientos son un concepto muy flexible para agregar información y mecánicas a tu mundo, y recomendamos utilizarlos en muchas situaciones.
Realmente no hay nada difícil en nuestra definición inicial de TChessPieceBehavior. Es casi una clase vacía. Decidí agregar solo un campo Booleano que indique si la pieza de ajedrez es blanca o negra:
1 2 3 4 5 |
type TChessPieceBehavior = class(TCastleBehavior) public Black: Boolean; end; |
Puedes declararlo al principio de la sección de interfaz de la unidad GameViewMain. Aunque las clases de comportamiento más grandes podrían merecer ser colocadas en sus propias unidades.
¿Cómo adjuntar las instancias de comportamiento a las escenas?
- Puedes hacerlo visualmente registrando la clase TChessPieceBehavior en el editor de Castle Game Engine. Este método es muy poderoso porque te permite añadir y configurar visualmente las propiedades del comportamiento. Consulta https://castle-engine.io/custom_components para ver cómo utilizarlo.
- O puedes hacerlo desde el código. En este artículo, he decidido seguir este enfoque. Esto es un poco más fácil si necesitas adjuntar efectivamente el comportamiento 32 veces, a todas las piezas de ajedrez, y no es necesario configurar específicamente el estado inicial del comportamiento. Hacer clic 32 veces en «Agregar Comportamiento» sería un poco tedioso y también innecesario en nuestro caso simple (para esta demostración, todas las piezas de ajedrez realmente funcionan de la misma manera), así que en su lugar utilizaremos código para inicializar fácilmente las piezas de ajedrez.
Para adjuntar un comportamiento a nuestra SceneBlackKing1, simplemente crearíamos una instancia de TChessPieceBehavior en el método Start de nuestra vista, y la añadiríamos usando SceneBlackKing1.AddBehavior. Así:
1 2 3 4 5 6 7 8 9 |
procedure TViewMain.Start; var ChessPiece: TChessPieceBehavior; begin inherited; ChessPiece := TChessPieceBehavior.Create(FreeAtStop); ChessPiece.Black := true; SceneBlackKing1.AddBehavior(ChessPiece); end; |
Pero esto no es lo suficientemente bueno para nuestra aplicación. Anteriormente agregamos TChessPieceBehavior solo a una pieza de ajedrez. Queremos agregarlo a las 32 piezas de ajedrez. ¿Cómo podemos hacerlo fácilmente? Necesitamos de alguna manera iterar sobre todas las piezas de ajedrez. Y para establecer el campo booleano Black, también debemos saber de alguna manera si esta es una pieza blanca o negra. Hay múltiples soluciones:
- Podríamos suponer que todas las piezas de ajedrez tienen nombres como SceneWhiteXxx o SceneBlackXxx. Entonces podríamos iterar sobre los hijos de Viewport1.Items y verificar si su nombre comienza con el prefijo dado.
- O podríamos observar el valor de la etiqueta (Tag) de las escenas, y tener una convención, por ejemplo, que Tag = 1 significa pieza de ajedrez negra, Tag = 2 significa pieza de ajedrez blanca, y otras etiquetas (Tag = 0 es el predeterminado, en particular) significan que no es una pieza de ajedrez.
- También podríamos introducir componentes de transformación adicionales que agrupen las piezas de ajedrez negras por separado de las piezas de ajedrez blancas y por separado de otros elementos (como el tablero de ajedrez).
Decidí seguir con este último enfoque, ya que la introducción de «componentes adicionales TCastleTransform para agrupar los existentes» es un mecanismo poderoso en muchas otras situaciones. Por ejemplo, luego puedes ocultar o mostrar fácilmente un grupo dado utilizando la propiedad Exists de TCastleTransform.
Para hacer esto, haz clic derecho en Viewport1.Items y elige en el menú contextual «Agregar Transformación → Transformación (TCastleTransform)».
Nombre este nuevo componente como «PiezasNegras». Luego, arrastra y suelta en la jerarquía del editor todas las piezas de ajedrez negras (componentes SceneBlackXxx) para que sean hijos de PiezasNegras. Puedes seleccionar fácilmente las 16 escenas que representan las piezas negras en la jerarquía manteniendo presionada la tecla Shift y luego arrastrándolas todas juntas dentro de PiezasNegras.
El resultado final debería ser algo así en la jerarquía:
No te preocupes si solo SceneBlackKing1 tiene los componentes de física. Pronto también configuraremos los componentes de física usando código.
Ahora repite el proceso para agregar un grupo WhitePieces.
Esta preparación en el editor facilita nuestra tarea de código. Agrega a la sección publicada de la declaración de TViewMain los campos BlackPieces y WhitePieces, de tipo TCastleTransform:
1 2 3 4 |
TViewMain = class(TCastleView) published ... // manten los otros campos BlackPieces, WhitePieces: TCastleTransform; |
Ahora itera sobre los grupos de las 2 piezas de ajedrez en el método Start:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
procedure TViewMain.Start; procedure ConfigureChessPiece(const Child: TCastleTransform; const Black: Boolean); var ChessPiece: TChessPieceBehavior; begin ChessPiece := TChessPieceBehavior.Create(FreeAtStop); ChessPiece.Black := true; Child.AddBehavior(ChessPiece); end; var Child: TCastleTransform; begin inherited; for Child in BlackPieces do ConfigureChessPiece(Child, true); for Child in WhitePieces do ConfigureChessPiece(Child, false); end; |
Sería prudente añadir una «verificación básica» en este punto. Vamos a registrar el número de piezas de ajedrez que tiene cada lado. Añade el siguiente código al final del método Start:
1 2 3 4 |
WritelnLog('Configured %d black and %d white chess pieces', [ BlackPieces.Count, WhitePieces.Count ]); |
Para hacer que WritelnLog esté disponible, añade la unidad CastleLog a la cláusula uses. Ahora, al ejecutar el juego, deberías ver un registro.
Configured 16 black and 16 white chess pieces
En mi primera ejecución, me di cuenta de que tenía 17 piezas de ajedrez en cada lado por accidente. Por error, añadí 3 caballos en lugar de 2 (uno de los caballos estaba exactamente en la misma posición que otro, por lo que no era obvio). Gracias a este registro, eliminé las piezas de caballo adicionales. Detectar este tipo de errores es precisamente la razón por la cual añadimos registros y realizamos pruebas, así que te animo a que también lo hagas.
Aprovechando esta oportunidad, podemos asegurarnos de que todas las piezas de ajedrez tengan componentes físicos (TCastleRigidBody y TCastleBoxCollider). Así que no necesitas añadirlos manualmente a cada una. Este enfoque es razonable si los componentes no necesitan ajustes manuales específicos por cada pieza de ajedrez.
Para hacer esto, ampliemos nuestro método ConfigureChessPiece:
1 2 3 4 5 6 7 8 |
procedure ConfigureChessPiece(const Child: TCastleTransform; const Black: Boolean); begin ... // keep previous code too if Child.FindBehavior(TCastleRigidBody) = nil then Child.AddBehavior(TCastleRigidBody.Create(FreeAtStop)); if Child.FindBehavior(TCastleCollider) = nil then Child.AddBehavior(TCastleBoxCollider.Create(FreeAtStop)); end; |
Como ves arriba, este enfoque es bastante directo: si no tienes el componente necesario, simplemente agrégalo. No nos molestamos en configurar ninguna propiedad en las nuevas instancias de TCastleRigidBody y TCastleBoxCollider, ya que sus valores predeterminados son adecuados para nuestro propósito.
Todo esto fue un buen «trabajo preliminar» para la parte restante del artículo. En realidad, no ha ocurrido ningún cambio funcional en nuestro juego; deberías ejecutarlo y ver que… nada ha cambiado. Las 32 piezas de ajedrez siguen en su lugar, al principio.
Seleccionar objetos 3D usando el mouse
Resaltar la pieza de ajedrez bajo el mouse y permitir seleccionarla
Para implementar la interacción real, queremos permitir al usuario elegir qué pieza de ajedrez lanzar usando el mouse. Castle Game Engine proporciona una función lista que te indica qué objeto está siendo señalado por la posición actual del mouse (o la última pulsación en dispositivos móviles). Esta función es TCastleViewport.TransformUnderMouse.
Para empezar, asegúrate de declarar la instancia del viewport en la sección publicada de la clase TViewMain, de esta manera:
1 |
MainViewport: TCastleViewport; |
Asegúrate de que el nombre de tu viewport coincida con el designado en el diseño. Agrega la unidad CastleViewport a la cláusula uses para hacer que el tipo TCastleViewport sea conocido.
Vamos a utilizar esta función para resaltar la pieza de ajedrez actual en la posición del mouse. Simplemente seguiremos verificando el valor de MainViewport.TransformUnderMouse en cada llamada a Update.
Nota: Alternativamente, podríamos verificar MainViewport.TransformUnderMouse en cada llamada a Motion, que ocurre solo cuando cambia la posición del mouse (o el toque). Pero hacerlo en Update es un poco mejor: como estamos utilizando física, algunas piezas de ajedrez aún pueden estar en movimiento debido a la física, por lo que la pieza de ajedrez bajo el mouse puede cambiar incluso si la posición del mouse no cambia.
Para mostrar realmente el resaltado, utilizaremos un efecto listo disponible para cada TCastleScene que se puede activar configurando MyScene.RenderOptions.WireframeEffect con algo distinto de weNormal. Esta es la forma más sencilla de mostrar el resaltado (discutiremos otras formas en una sección posterior).
Antes de pasar al código, te animo a experimentar con la configuración perfecta de RenderOptions para el resaltado en el editor. Simplemente edita cualquier pieza de ajedrez elegida hasta que tenga un resaltado atractivo, y recuerda las opciones elegidas. Las propiedades más útiles para ajustar son WireframeEffect, WireframeColor, LineWidth, SilhouetteBias, SilhouetteScale. Puedes verlas enfatizadas abajo: el editor muestra las propiedades que tienen valores no predeterminados usando una fuente en negrita.
He decidido mostrar la pieza de ajedrez actualmente resaltada (en la posición del mouse) con un alambre de color azul claro. Esta pieza de ajedrez también se establece como el valor del campo privado ChessPieceHover.
Además, una vez que el usuario hace clic con el mouse (podemos detectarlo en Press), la pieza de ajedrez se considera seleccionada y obtiene un resaltado amarillo. Esta pieza de ajedrez se establece como el valor de ChessPieceSelected.
Recordar los valores de ChessPieceHover y ChessPieceSelected es útil para algunas cosas. Por un lado, podemos desactivar el efecto más adelante (cuando la pieza ya no esté resaltada o seleccionada). Además, nos permitirá lanzar la pieza ChessPieceSelected en las próximas secciones.
Podríamos almacenarlos como referencias a TCastleScene o TChessPieceBehavior. Es decir, podríamos declarar:
- O bien ChessPieceHover, ChessPieceSelected: TChessPieceBehavior; …
- … o ChessPieceHover, ChessPieceSelected: TCastleScene;
Ambas declaraciones serían válidas para nuestra aplicación. Es decir, tenemos que elegir una u otra ya que implicará un código un poco diferente, pero las diferencias son realmente menores. Al final, siempre podemos obtener una instancia de TChessPieceBehavior de un TCastleScene correspondiente (si sabemos que es una pieza de ajedrez) y podemos obtener un TCastleScene de un TChessPieceBehavior.
Para obtener TChessPieceBehavior del TCastleScene correspondiente, haríamos lo siguiente:
1 2 3 4 5 6 |
var MyBehavior: TChessPieceBehavior; MyScene: TCastleScene; begin ... MyBehavior := MyScene.FindBehavior(TChessPieceBehavior) as TChessPieceBehavior; |
Para obtener TCastleScene a partir de un TChessPieceBehavior correspondiente, harías lo siguiente:
1 2 3 4 5 6 |
var MyBehavior: TChessPieceBehavior; MyScene: TCastleScene; begin ... MyScene := MyBehavior.Parent as TCastleScene; |
Si decides seguir mi enfoque exactamente, agrega esto a la sección privada de la clase TViewMain:
1 2 3 4 5 |
ChessPieceHover, ChessPieceSelected: TChessPieceBehavior; { Turn on / off the highlight effect, depending on whether Behavior equals ChessPieceHover, ChessPieceSelected or none of them. This accepts (and ignores) Behavior = nil value. } procedure ConfigureEffect(const Behavior: TChessPieceBehavior); |
Entonces agrega la unidad CastleColors a la cláusula uses (ya sea en la interfaz o la implementación) de la unidad GameViewMain, no importa en este caso, para definir la utilidad HexToColorRGB.
Finalmente, este es el código actualizado de los métodos Update, Press y del ayudante ConfigureEffect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
procedure TViewMain.ConfigureEffect(const Behavior: TChessPieceBehavior); var Scene: TCastleScene; begin if Behavior = nil then Exit; { Behavior can be attached to any TCastleTransform. But in our case, we know TChessPieceBehavior is attached to TCastleScene. } Scene := Behavior.Parent as TCastleScene; if (Behavior = ChessPieceHover) or (Behavior = ChessPieceSelected) then begin Scene.RenderOptions.WireframeEffect := weSilhouette; if Behavior = ChessPieceSelected then Scene.RenderOptions.WireframeColor := HexToColorRGB('FFEB00') else Scene.RenderOptions.WireframeColor := HexToColorRGB('5455FF'); Scene.RenderOptions.LineWidth := 10; Scene.RenderOptions.SilhouetteBias := 20; Scene.RenderOptions.SilhouetteScale := 20; end else begin Scene.RenderOptions.WireframeEffect := weNormal; end; end; procedure TViewMain.Update(const SecondsPassed: Single; var HandleInput: Boolean); var OldHover: TChessPieceBehavior; begin inherited; LabelFps.Caption := 'FPS: ' + Container.Fps.ToString; OldHover := ChessPieceHover; if MainViewport.TransformUnderMouse <> nil then begin ChessPieceHover := MainViewport.TransformUnderMouse.FindBehavior(TChessPieceBehavior) as TChessPieceBehavior; end else ChessPieceHover := nil; if OldHover <> ChessPieceHover then begin ConfigureEffect(OldHover); ConfigureEffect(ChessPieceHover); end; end; function TViewMain.Press(const Event: TInputPressRelease): Boolean; var MyBody: TCastleRigidBody; OldSelected: TChessPieceBehavior; begin Result := inherited; if Result then Exit; // allow the ancestor to handle keys // ... if you want, keep here the handling of keyX from previous exercise if Event.IsMouseButton(buttonLeft) then begin OldSelected := ChessPieceSelected; if (ChessPieceHover <> nil) and (ChessPieceHover <> ChessPieceSelected) then begin ChessPieceSelected := ChessPieceHover; ConfigureEffect(OldSelected); ConfigureEffect(ChessPieceSelected); end; Exit(true); // mouse click was handled end; end; |
Como siempre, recuerda compilar y ejecutar el código para asegurarte de que funcione correctamente.
Notarás que MainViewport.TransformUnderMouse detecta lo que está debajo del mouse, pero trata a cada pieza de ajedrez como una caja. Por lo tanto, la detección no es precisa visualmente. Para corregir esto, establece PreciseCollisions en true en todas las piezas de ajedrez. Puedes hacer esto fácilmente seleccionando todas las piezas de ajedrez en el editor usando Shift o Ctrl y luego cambiando PreciseCollisions en el Inspector de Objetos.
He decidido mover la cámara en este punto también (para mostrar ambas partes, negra y blanca, desde una vista lateral).
Nota adicional: Otras formas de mostrar un resaltado
Existen otras formas de resaltar la pieza de ajedrez seleccionada (o resaltada).
- Cambiar dinámicamente el color del material. Esto se logra accediendo a una instancia de TPhysicalMaterialNode dentro de los nodos de la escena (TCastleScene.RootNode) y modificando TPhysicalMaterialNode.BaseColor. Consulta por ejemplo el ejemplo del motor en examples/viewport_and_scenes/collisions/ que utiliza este enfoque.
- Añadir/eliminar dinámicamente un efecto de sombreado. Esto implica agregar nodos TEffectNode y TEffectPartNode a la escena e implementar el efecto usando GLSL (Lenguaje de Sombreado de OpenGL). Consulta por ejemplo el ejemplo del motor en examples/viewport_and_scenes/shader_effects/ que demuestra este método.
- Añadir una caja adicional que rodee el objeto elegido. El editor CGE mismo utiliza esta técnica para mostrar objetos 3D resaltados/seleccionados. Utiliza la clase TDebugTransformBox para implementar esto fácilmente.
Si tienes curiosidad, espero que la información y ejemplos anteriores te orienten en la dirección correcta.
Nota adicional: Sombras
He decidido activar las sombras en este punto. Simplemente establece Shadows en true en la fuente de luz principal. Además, establece RenderOptions.WholeSceneManifold en true en las piezas de ajedrez. Esto debería hacer que todo proyecte sombras agradables. Las sombras son dinámicas, lo que significa que cambiarán adecuadamente cuando movamos las piezas de ajedrez.
Consulta https://castle-engine.io/shadow_volumes para obtener más información sobre sombras en Castle Game Engine.
Permitir al usuario elegir el ángulo y la fuerza para lanzar la pieza de ajedrez
Una vez que el usuario ha seleccionado una pieza de ajedrez, queremos permitirle configurar la dirección y la fuerza con la que lanzar el objeto elegido. Ya sabemos que «lanzar» la pieza de ajedrez técnicamente significa «aplicar una fuerza física al cuerpo rígido de la pieza de ajedrez seleccionada». Casi tenemos todo lo que necesitamos, pero necesitamos permitir que el usuario elija la dirección y la fuerza de esta fuerza.
Diseñando una flecha 3D
Para visualizar la fuerza deseada, utilizaremos un modelo simple de flecha 3D, que se rotará y escalará según corresponda. Aunque podríamos diseñar este modelo en Blender u otro software de autoría 3D, en este caso es más fácil hacerlo completamente en el editor Castle Game Engine. La flecha es una composición de dos formas simples: un cono (para la punta de la flecha) y un cilindro.
Además, diseñaremos la flecha de manera independiente, como un diseño separado. El nuevo diseño contendrá una jerarquía de componentes, con la raíz siendo TCastleTransform. Lo guardaremos como un archivo force_gizmo.castle-transform en el subdirectorio de datos del proyecto. Luego lo añadiremos al diseño principal (gameviewmain.castle-user-interface) y alternaremos la existencia, rotación y escala de la fuerza visualizada.
Utilizar un archivo de diseño separado para la flecha 3D, aunque no sea estrictamente necesario en este caso, es una técnica poderosa. Cuando algo se guarda como un archivo de diseño separado, se puede reutilizar libremente e instanciar muchas veces (en tiempo de diseño o mediante la creación dinámica durante la ejecución del juego). Esto es, por ejemplo, cómo tener criaturas en tu juego: objetos 3D que comparten lógica común y que pueden generarse cuando sea necesario.
Para comenzar a diseñar la flecha, elige el elemento de menú del editor «Diseño → Nuevo Transform (Transformación vacía como raíz)».
Debajo de esto, agrega dos componentes: TCastleCylinder y TCastleCone.
Ajusta su Altura, Radio (en el cilindro), RadioBase (en el cono) y su Posición para formar una flecha 3D agradable.
Ajusta su Color a algo diferente al predeterminado para embellecer las cosas. Recuerda que la flecha será iluminada por las luces que hemos configurado en el diseño principal (gameviewmain.castle-user-interface), por lo que probablemente será más brillante de lo que observas ahora.
Puedes seguir los valores que he elegido en las capturas de pantalla a continuación, pero en realidad son solo ejemplos. Adelante y crea tu propia flecha 3D como prefieras.
Ahora viene una parte un poco difícil. Queremos tener una flecha que pueda rotar fácilmente alrededor de una caja ficticia (en el juego real, rotará alrededor de una pieza de ajedrez). Idealmente, la flecha también debería escalar fácilmente para visualizar la fuerza de empuje. Uso la palabra «fácilmente» para enfatizar que no queremos solo rotarla en el editor, sino que también permitiremos al usuario rotarla durante el juego. Por lo tanto, la rotación y la escala que nos interesan deben ser muy fáciles de obtener y establecer desde el código.
Para hacer esto, primero agrega una caja ficticia que represente una pieza de ajedrez. Yo la llamé DebugBoxToBeHidden y configuré su tamaño a 2 3 2 para tener en cuenta piezas altas (eje Y grande). Más tarde haremos que la caja sea invisible estableciendo su propiedad Exists en false.
Una vez que tengas la caja, quieres agregar componentes TCastleTransform intermedios para:
- Rotar la flecha (cono y cilindro) para que sea horizontal.
- Mover la flecha lejos de la caja.
- Rotar la flecha alrededor de la caja.
- Escalar la flecha.
Hay múltiples formas válidas de lograr esto. El consejo clave es no dudar en hacer una composición anidada, es decir, colocar TCastleTransform dentro de otro TCastleTransform y así sucesivamente. Permite que cada TCastleTransform realice una sola función. Tómatelo paso a paso y llegarás a una solución válida (y realmente hay varias formas posibles de organizar esto).
Mira mi disposición en las capturas de pantalla a continuación. Si te quedas atascado, simplemente usa el diseño de nuestro proyecto resultante en https://github.com/castle-engine/bad-chess/ (en el subdirectorio project/version_2_with_code).
El resultado de mi diseño es que sé que desde el código puedo:
- Ajustar la propiedad Rotation del componente TransformForceAngle para que sea una simple rotación alrededor del eje X. El ángulo de esta rotación puede ser elegido por el usuario y efectivamente la flecha orbitará alrededor de la caja ficticia (pieza de ajedrez).
- Ajustar Y de la propiedad Scale del componente TransformForceStrength. El usuario puede elegir la cantidad de esta escala para visualizar la fuerza.
Recuerda establecer Exists del componente DebugBoxToBeHidden en false una vez que hayas terminado.
Añade la flecha al diseño principal.
Para probar que funciona, agrega el diseño de la flecha al diseño principal usando el editor.
Guarda el diseño force_gizmo.castle-transform, abre nuestro diseño principal en gameviewmain.castle-user-interface, selecciona el componente Items dentro de MainViewport y arrastra y suelta el archivo force_gizmo.castle-transform (desde el panel «Files» abajo) en la jerarquía.
El resultado debería ser que se cree un nuevo componente llamado DesignForceGizmo1 y se coloque como hijo de Items. La clase del componente es TCastleTransformDesign, lo que significa que es una instancia de TCastleTransform cargada desde otro archivo con extensión .castle-transform. La propiedad URL de este componente debería establecerse automáticamente para indicar nuestro archivo force_gizmo.castle-transform.
Renombra este componente simplemente como DesignForceGizmo (como prefieras, pero creo que hace las cosas más claras, ya que solo necesitaremos un gizmo de este tipo). Además, cambia la propiedad Exists de este componente a false porque inicialmente no queremos que este componente sea visible ni seleccionable por el ratón.
La captura de pantalla a continuación muestra el estado justo antes de establecer Exists en false.
Permitir al usuario controlar la flecha
Necesitamos declarar e inicializar los campos que describen el ángulo y la fuerza actuales.
Agrega esto a la sección privada de la clase TViewMain:
1 2 3 4 5 6 7 8 9 |
const MinStrength = 1; MaxStrength = 1000; MinStrengthScale = 1; MaxStrengthScale = 3; StrengthChangeSpeed = 30; AngleAChangeSpeed = 10; |
Agrega a la cláusula uses las nuevas unidades necesarias: Math, CastleUtils.
Finalmente, añade al método Start de TViewMain el código adicional para inicializar todo:
1 2 3 4 5 6 7 8 9 10 |
TransformForceAngle := DesignForceGizmo.DesignedComponent('TransformForceAngle') as TCastleTransform; TransformForceStrength := DesignForceGizmo.DesignedComponent('TransformForceStrength') as TCastleTransform; ForceAngle := 0; // 0 is default value of Single field anyway TransformForceAngle.Rotation := Vector4(1, 0, 0, ForceAngle); ForceStrength := 10; // set some sensible initial value TransformForceStrength.Scale := Vector3(1, MapRange(ForceStrength, MinStrength, MaxStrength, MinStrengthScale, MaxStrengthScale), 1); |
Ten en cuenta que inicializamos los componentes dentro de nuestro diseño DesignForceGizmo usando la llamada DesignForceGizmo.DesignedComponent(…). Esto es necesario, ya que en general puedes tener múltiples instancias del diseño force_gizmo.castle-transform colocadas en tu vista. Por lo tanto, los campos publicados de la vista no pueden asociarse automáticamente con componentes en diseños anidados.
Además, sincronizamos los campos Single ForceStrength y ForceAngle con sus instancias correspondientes TCastleTransform. Single en Pascal es un número de punto flotante simple, que es muy fácil de manipular. Tratamos las dos instancias TCastleTransform mencionadas anteriormente como una forma elegante de visualizar estos números como rotación y escala en 3D.
Puede que desees consultar qué hace la función MapRange en la referencia de la API de Castle Game Engine. En resumen, es una forma cómoda de realizar una interpolación lineal, convirtiendo de un rango a otro.
Ahora que hemos inicializado todo, vamos a mostrar realmente el DesignForceGizmo cuando el usuario selecciona una pieza de ajedrez. Ya tenemos un código para seleccionar la pieza de ajedrez con un clic de ratón. Solo modifícalo para mostrar el DesignForceGizmo y reposicionarlo en la pieza de ajedrez seleccionada.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if Event.IsMouseButton(buttonLeft) then begin OldSelected := ChessPieceSelected; if (ChessPieceHover <> nil) and (ChessPieceHover <> ChessPieceSelected) then begin ... // conserva el código anterior // new lines: DesignForceGizmo.Exists := true; DesignForceGizmo.Translation := ChessPieceSelected.Parent.WorldTranslation; end; Exit(true); // el click del ratón ha sido gestionado end; |
Nota: Puede que te preguntes acerca de un enfoque alternativo, donde no reposicionamos DesignForceGizmo, sino que en su lugar cambiamos dinámicamente su padre, como en DesignForceGizmo.Parent := ChessPieceSelected.Parent. Esto también funcionaría, aunque con algunas complicaciones adicionales: la rotación del objeto seleccionado, una vez que lo impulsamos, también rotaría el gizmo. Esto complicaría el cálculo de la «dirección de impulso deseada» más adelante. Por eso decidí optar por el enfoque más simple de simplemente reposicionar el DesignForceGizmo. Si deseas experimentar con el enfoque complicado alternativo, adelante; una solución sería diseñar DesignForceGizmo de manera que luego puedas hacer TransformForceAngle.GetWorldView(WorldPos, WorldDir, WorldUp) y usar WorldDir resultante como dirección de fuerza.
Pero como mantenemos las cosas simples… casi hemos terminado. Puedes ejecutar el juego y ver que seleccionar una pieza de ajedrez muestra correctamente el gizmo de flecha. Solo queda permitir al usuario cambiar la dirección y la fuerza. Podemos hacer esto observando las teclas que el usuario presiona en el método Update. El código a continuación permite rotar la flecha (hacer que orbite alrededor de la pieza de ajedrez) usando las teclas de flecha izquierda y derecha, y cambiar la fuerza del impulso (escalar la flecha) usando las teclas de flecha arriba y abajo. Añade este código a tu método Update existente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TViewMain.Update(const SecondsPassed: Single; var HandleInput: Boolean); begin ... // keep existing code if Container.Pressed[keyArrowLeft] then ForceAngle := ForceAngle - SecondsPassed * AngleAChangeSpeed; if Container.Pressed[keyArrowRight] then ForceAngle := ForceAngle + SecondsPassed * AngleAChangeSpeed; if Container.Pressed[keyArrowUp] then ForceStrength := Min(MaxStrength, ForceStrength + SecondsPassed * StrengthChangeSpeed); if Container.Pressed[keyArrowDown] then ForceStrength := Max(MinStrength, ForceStrength - SecondsPassed * StrengthChangeSpeed); TransformForceAngle.Rotation := Vector4(1, 0, 0, ForceAngle); TransformForceStrength.Scale := Vector3(1, MapRange(ForceStrength, MinStrength, MaxStrength, MinStrengthScale, MaxStrengthScale), 1); end; |
¡Lanza esa pieza de ajedrez!
«Parece que tenemos todo el conocimiento que necesitamos.
- Sabemos cómo lanzar la pieza de ajedrez,
- Sabemos qué pieza de ajedrez lanzar,
- Conocemos la dirección y la fuerza del lanzamiento.
Puedes consultar el código que hicimos algunas secciones antes, en el ejercicio ‘Push the chess piece using physics’. Nuestro nuevo código será similar. Agrégalo a la implementación del método Press:»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function TViewMain.Press(const Event: TInputPressRelease): Boolean; var ... // keep existing variables used by other inputs ChessPieceSelectedScene: TCastleScene; ForceDirection: TVector3; begin Result := inherited; if Result then Exit; // allow the ancestor to handle keys ... // conserva el código anterior que gestiona las entradas if Event.IsKey(keyEnter) and (ChessPieceSelected <> nil) then begin ChessPieceSelectedScene := ChessPieceSelected.Parent as TCastleScene; MyBody := ChessPieceSelectedScene.FindBehavior(TCastleRigidBody) as TCastleRigidBody; ForceDirection := RotatePointAroundAxis( Vector4(0, 1, 0, ForceAngle), Vector3(-1, 0, 0)); MyBody.ApplyImpulse( ForceDirection * ForceStrength, ChessPieceSelectedScene.WorldTranslation); // unselect after flicking; not strictly necessary, but looks better ChessPieceSelected := nil; DesignForceGizmo.Exists := false; Exit(true); // entrada gestionada end; end; |
«Dependiendo de cómo hayas diseñado el diseño force_gizmo.castle-transform, es posible que necesites ajustar el cálculo de ForceDirection, en particular el segundo parámetro de RotatePointAroundAxis que es la dirección utilizada cuando el ángulo es cero. No hay nada mágico en nuestro valor Vector3(-1, 0, 0), simplemente sigue el diseño de nuestro force_gizmo.castle-transform.
¡Ejecuta el juego y verifica que ahora puedes lanzar las piezas de ajedrez!
- Selecciona la pieza de ajedrez haciendo clic con el ratón.
- Rota la fuerza con las teclas de flecha izquierda y derecha.
- Cambia la fuerza con las teclas de flecha arriba y abajo.
- Lanza la pieza de ajedrez presionando Enter.
- ¡Repite :)»
Conclusión e ideas para el futuro
¡Invita a un amigo a jugar contigo! Hagan turnos usando el mouse para lanzar sus piezas de ajedrez y diviértanse 🙂
Estoy seguro de que ahora puedes inventar múltiples formas de hacer esto aún mejor.
- ¿Quizás cada jugador debería poder lanzar solo sus propias piezas de ajedrez? Ya sabemos qué pieza de ajedrez es negra o blanca (el campo booleano Black en TChessPieceBehavior), aunque no lo usamos para nada anteriormente. Debes rastrear qué jugador lanzó el objeto por última vez (negro o blanco) y solo permitir elegir al lado opuesto la próxima vez.
- ¿Quizás quieras mostrar alguna interfaz de usuario, como una etiqueta, para indicar de quién es el turno? Simplemente coloca un componente TCastleLabel en la vista y cambia la leyenda cuando lo desees.
- ¿Quizás quieras mostrar el ángulo y la fuerza actuales — ya sea como números o como barras de colores? Usa TCastleRectangleColor para un rectángulo trivial con borde opcional y opcionalmente relleno de un color.
- ¿Quizás quieras implementar un juego de ajedrez real? Claro, simplemente sigue en el código todas las piezas de ajedrez y las casillas del tablero de ajedrez — qué está donde. Luego agrega una lógica que permita al jugador seleccionar qué pieza y dónde mover. Agrega alguna validación. Agrega juego con un oponente informático si lo deseas — existen protocolos estandarizados para comunicarse con «motores de ajedrez», por lo que no necesitas implementar tu propia inteligencia artificial de ajedrez desde cero.
- ¿Quizás quieras usar redes? Puedes usar varias soluciones de redes (cualquier biblioteca Pascal) junto con Castle Game Engine. Consulta https://castle-engine.io/manual_network.php. Hemos utilizado el motor con Indy y RNL (Realtime Network Library). En el futuro, planeamos integrar el motor con Nakama, un marco de servidor y cliente de código abierto para juegos multijugador.
- ¿Quizás quieras implementar este juego en otras plataformas, en particular en móviles? Adelante. El código que escribimos anteriormente ya es multiplataforma y se puede compilar con Castle Game Engine para cualquier dispositivo Android o iOS. Nuestra herramienta de compilación hace todo por ti, obtienes un archivo APK, AAB o IPA listo para instalar en tu teléfono. Consulta la documentación del motor en https://castle-engine.io/manual_cross_platform.php.
- Aunque los controles de teclado no funcionarán en móviles. Necesitas inventar e implementar una nueva interfaz de usuario para girar la fuerza, cambiar la fuerza y lanzar realmente la pieza de ajedrez. Lo más sencillo es mostrar botones clicables para realizar las acciones relevantes. La clase TCastleButton del motor es un botón con una apariencia completamente personalizable.
Si deseas aprender más sobre el motor, lee la documentación en https://castle-engine.io/ y únete a nuestra comunidad en el foro y Discord: https://castle-engine.io/talk.php. Por último, pero no menos importante, si te gusta este artículo y el motor, apreciaremos tu apoyo en Patreon https://www.patreon.com/castleengine. Realmente contamos con tu apoyo.
Finalmente, sobre todo, ¡diviértete! Crear juegos es un proceso emocionante y experimentar con la sensación de «lo que se siente bien» es la forma correcta de hacerlo. ¡Espero que lo disfrutes!