En esta nueva entrada haremos que Arthur dispare su arma. Para ello veremos como crear un objeto por código, como gestionarlo, y como gestionar un temporizador.
Arthur tiene como modo de defensa la capacidad de lanzar espadas. Solo puede lanzar como máximo dos a la vez, y las espadas vuelan paralelas al suelo. Teniendo esto en cuenta vamos a continuar programando el juego e implementaremos esto.
Copia el archivo spear.png dentro de la carpeta sprites, Crea una nueva hoja de sprites y añade el archivo que acabas de copiar. Crea 4 animaciones, cada una con de la imágenes.

Cómo el arma será un elemento que aparecerá en el juego por momentos y luego desparece, vamos a crearlo por código.
Contenido
Creación del arma por código.
En la sección private añade dos elementos TcastleScene. Añadimos dos elementos, porque uno sera el arma, y el otro será el arma rotada 180. Para poder tener un elemento para cada dirección de disparo

Siguiendo con la idea de tener lo más ordenado posible el código crearemos un procedimiento dónde escribiremos el código necesario para inicializar las armas.
En la sección Private, crea un procedimiento llamado ConfigureWeaponSpriteScene, puedes pulsar control+shift+c para que Lazarus cree el cuerpo del procedimiento automáticamente.

En el procedimiento escribimos el siguiente código.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
procedure TViewMain.ConfigureWeaponSpriteScene; begin WeaponSpriteScene := TCastleScene.Create(FreeAtStop); WeaponSpriteScene.Url := 'castle-data:/sprites/spear.castle-sprite-sheet'; WeaponSpriteScene.AutoAnimation := 'Spear1'; WeaponSpriteScene.Scale := Vector3(1, 1, 1); WeaponSpriteSceneRotate := TCastleScene.Create(FreeAtStop); WeaponSpriteSceneRotate.Url := 'castle-data:/sprites/spear.castle-sprite-sheet'; WeaponSpriteSceneRotate.AutoAnimation := 'Spear1'; WeaponSpriteSceneRotate.Scale := Vector3(1, 1, 1); WeaponSpriteSceneRotate.Rotation := Vector4(0, 1, 0, Deg(180)); end; |
Básicamente lo hace este código, es crear los elemento que hemos declarado en la sección Private. Es importante que al crearlo le indiquemos el parámetro FreeAtStop de esta manera el motor gráfico será el encargado de liberar de la memoria el objeto cuando le indiquemos que ya no lo usaremos más.
La siguiente línea le asignamos la hoja de sprites que debe cargar. Después le indicamos que animación debe ejecutar, y por último le indicamos que su lo represente a una escala 1,1,1. Esto es que su tamaño original.
Estas tres líneas son iguales, tanto para una arma como para su “homologa” rotada. Pero el arma que usaremos rotada, debemos indicarle que la rote. Esto se hace indicando una rotación, de la misma manera que hicimos con el personaje del juego.
Por último debemos llamar a este procedimiento desde el procedimiento Start.

Para cumplir el resto de las condiciones que se comentaron antes, debemos definir las siguientes variables y temporizador, las cuales las declararemos en la sección private

La variable CanShot(1), la usaremos para indicar si se puede disparar. En el caso, por ejemplo, de que ya haya dos espadas en el escenario, esta variable se valdrá falso.
La variable, TimerShot(2), es un temporizador, el cual lo usaremos para temporizar el lanzamiento. Para que el jugador no pulse la tecla de disparo y se disparen múltiples espadas.
Cuando lanzamos una espada, realizamos diversas operaciones, crear el sprite, etc. Para saber que estamos haciendo “la preparación” lo indicamos con la variable Shotting(3).
La variable NumeroDisparos(4), indica cuantas espadas están actualmente en el escenario.
Por último, el procedimiento OnTimerShot(5) es un procedimiento que se será llamado cada vez que se cumpla el tiempo que especificamos en el temporizador. No es necesario que lo escribas ahora esta línea, ya que cuando inicialicemos el temporizador, Lazarus lo creará por nosotros
En procedimiento Onstart, debemos inicializar todas estas variables, y preparar el temporizador
El siguiente código se encarga de ello:
|
1 2 3 4 5 6 7 8 9 10 11 |
TimerShot := TCastleTimer.Create(FreeAtStop); {$IFDEF FPC} TimerShot.OnTimer:=@OnTimerShot; {$ELSE} TimerShot.OnTimer := OnTimerShot; {$ENDIF} TimerShot.IntervalSeconds := 1; CanShot := True; shotting := False; InsertBack(TimerShot); NumeroDisparos := 0; |
Lo primero es crear el temporizador, que es un objeto de tipo TcastleTimer.
Le asignamos el evento OnTimer, el cual se ejecutará cada vez que el temporizador alcance el valor establecido. Recuerda que tras escribir la línea, su pulsas Ctrl+shift+C, lazarus creará el procedimiento asociado, así como su declaración.
Asignamos a la propiedad InvervalSeconds, el valor 1 que será el intervalo en segundos de nuestro temporizador.
Asignamos las variables booleanas a su estado inicial. Enviamos el temporizador “al fondo” para “congelarlo”. Por último inicializamos la variable NumeroDisparos a cero.
Disparando
Con estas líneas , al iniciar el juego, tendremos todo preparado para empezar a disparar
El siguiente paso es modificar la función FireWeapon
|
1 2 3 4 5 6 7 8 9 |
function TViewMain.FireWeapon: boolean; begin Result := False; if (Container.Pressed[keySpace]) and (CanShot = True) and (NumeroDisparos <= 2) then begin Result := True; StarTimerShot; end; end; |
Modificamos la instrucción IF, de manera que tenga en cuenta la variable CanShot sea igual a TRUE y NúmeroDisparos sea menor igual a 2. Además llamamos al procedimiento StartTimerShot, el cual se encarga de iniciar el temporizador.
Crea el procedimiento StartTimeShot, en la sección Private, y escribe el siguiente código dentro de ella.
|
1 2 3 4 5 |
procedure TViewMain.StarTimerShot; begin CanShot := True; InsertFrontIfNotExists(TimerShot); end; |
Este procedimiento cambia la variable CanShot a TRUE y trae “al frente” el temporizador, con lo cual empieza a contar.
En el procedimiento Update, gestionaremos si la función FireWeapon devuelve TRUE. En tal caso debemos lanzar una espada.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
if FireWeapon then begin if shotting = False then begin TimerShot.ResetNextTimerEvent; shotting := True; NumeroDisparos := NumeroDisparos + 1; if Arthur.Rotation.W = 0 then begin VectorDireccion := Vector3(1, 0, 0); end else begin VectorDireccion := Vector3(-1, 0, 0); end; Shot(Arthur, Arthur.LocalToWorld(Vector3(Arthur.BoundingBox.SizeX / 2 + 5, 0, 0)), VectorDireccion); CanShot := False; shotting := False; end; end; |
Usaremos la variable, declarada en la sección Private, shotting la cual nos indicará si estamos lanzado la espada. En el caso que no sea así, reiniciamos el temporizador para asegurar que empieza a contar desde cero. Ponemos la variable Shotting a TRUE, incrementamos el contador de número de disparos. Y dependiendo hacía donde esté mirando el personaje, creamos un vector de dirección paralelo al eje X. El sentido de este vector está indicado por el signo de su valor en X.
Llamamos al procedimiento Shot (ahora entro en detalles sobre este procedimiento), ya por último indicamos que no se puede disparar cambiando a FALSE y cambiamos la variable Shotting a FALSE para indicar que hemos terminado el proceso de disparo.
La variable CanShot cambiará a TRUE, cuando haya pasado el tiempo que establecimos en el temporizador. Para ello en el evento OnTimerShot que creamos antes, debemos escribir el siguiente código.
|
1 2 3 4 5 |
procedure TViewMain.OnTimerShot(Sender: Tobject); begin InsertBackIfNotExists(TimerShot); CanShot := True; end; |
En este procedimiento lo primero en “enviar” el temporizador a dormir. Y cambiamos la variable CanShot a True.
Clase TWeapon
Este shot se encarga de realizar el disparo pero depende de la clase Tweapon, la cual será el arma que disparamos y se encargará de desplazarla y calcular cuando debe desaparecer.
La clase Tweapon, la debes declarar al principio de la unidad, justo debajo de dónde declaraste la clase TlevelBonus.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
type { TWeapon } TWeapon = class(TCastleTransform) strict private FMaxDistance: Single; FRBody: TCastleRigidBody; FStartPoint: TVector2; public constructor Create(AOwner: TComponent; WeaponSpriteScene: TCastleScene); reintroduce; procedure Update(const SecondsPassed: single; var RemoveMe: TRemoveType); override; property RBody: TCastleRigidBody read FRBody; property MaxDistance:Single read FMaxDistance write FMaxDistance; property StartPoint: TVector2 read FStartPoint write FStartPoint; end; |
Esta clase desciende, o es hija, de TcastleTransform. Declaramos un constructor, con el argumento Reintroduce, así podemos usar el método Create que ha sido escondido por la clase padre, añadiendo algunos argumentos más. En este método recibe, además del propietario (Aowner), que es obligatorio, un parámetro de tipo TcastleScene, que será nuestra arma.
Por otra parte declaramos el procedimiento Update, el cual será llamada cada vez que se debe actualizar nuestra arma. Este método es llamado automáticamente con el motor gráfico, al ser un objeto de desciende de TcastleTransform hereda esta carácteristica.
La propiedad Rbody, es de tipo TcastleRigiBody, la cual contendrá un RigiiBody, que es el objeto que gestionará las colisiones. Al igual que se lo hemos creado desde el editor de CGE, lo crearemos desde código.
La propiedad MaxDistance, la usaremos para almacenar la distancia máxima que puede recorrer el arma, desde el punto de inicio, que se guarda en la propiedad StartPoint. Al ser un punto en 2D, esta será un vector de tipo TVector2 .
Constructor
Veamos el código del constructor
|
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 |
constructor TWeapon.Create(AOwner: TComponent; WeaponSpriteScene: TCastleScene); var col: TCastleBoxCollider; begin inherited Create(AOwner); //Añadir elemento a la escena Add(WeaponSpriteScene); WeaponSpriteScene.Visible := True; WeaponSpriteScene.Translation := Vector3(0, 0, 1); { En este caso, se añade TCastleRigidBody a TWeapon(TCastleTransform) y no a WeaponSpriteScene para poder usar esta escena en varias armas } FRBody := TCastleRigidBody.Create(Self); FRBody.Setup2D; RBody.Dynamic := True; RBody.CollisionDetection := cdContinuous; RBody.MaxLinearVelocity := 0; RBody.Layer := 3; //Plano del protagonista RBody.Gravity := False; // Creamos el collider col := TCastleBoxCollider.Create(Self); col.Restitution := 0.0; Col.Mass := 1; AddBehavior(Col); AddBehavior(FRBody); end; |
Observa el código y sus comentarios. Empezamos añadiendo el arma, que hemos recibido por el parámetro WeaponSpriteScene, a la escena. Lo hacemos visible y lo situamos en el origen de coordenadas.
Después creamos el objeto TcastleRigiBody, ajustamos sus propiedades a un entorno 2D, su propiedad Dinamic la ponemos a True, para que su desplazamiento se gestionado por el motor de físicas. La detección de colisión (CollisionDectection) a continuo, la velocidad a cero, en el mismo plano del protagonista, e indicamos que no le afecte la gravedad.
Ya por último, creamos el volumen de colisión o collider.
Con todo esto listo, los añadimos a “comportamientos” de nuestro objeto.
Update
Por otra parte, tenemos el procedimiento Update
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
procedure TWeapon.Update(const SecondsPassed: single; var RemoveMe: TRemoveType); var Posicion: TVector2; begin inherited Update(SecondsPassed, RemoveMe); Posicion := FStartPoint - Self.translationXY; if (Abs(Posicion.x) > FMaxDistance) then begin Self.Visible := False; Self.Exists := False; RemoveMe := rtRemoveAndFree; end; end; |
En este procedimiento, comprobaremos si la posición actual, restada a la posición de inicio es mayor que FmaxDistance. En tal caso cambiamos las propiedades Visible y Exists a False. Y cambiando la variable RemoveMe, que hemos recibido como parámetro, al valor rtRemoveAndFree, indicamos al motor gráfico que debe borrar este objeto. El motivo de cambiar las variables Visible y Exists a False, es para que mientras no se destruye, no se visible para jugador y al “no existir” no se tendrá en cuenta para futuras colisiones
Procedimiento Shot.
En este procedimiento crearemos un objeto Tweapon, y lo inicializamos con los datos que necesitamos.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
procedure TViewMain.Shot(WeaponOwner: TComponent; const Origen, Direccion: TVector3); var Weapon: TWeapon; begin if Direccion.X < 0 then begin Weapon := TWeapon.Create(WeaponOwner, WeaponSpriteSceneRotate); end else begin Weapon := TWeapon.Create(WeaponOwner, WeaponSpriteScene); end; Weapon.StartPoint := Vector2(Origen.X,Origen.Y); Weapon.MaxDistance:=ViewPort1.Camera.Orthographic.EffectiveRect.Width / 2; Weapon.Translation := Origen; Weapon.RBody.LinearVelocity := Direccion * Vector3(150, 0, 0); ViewPort1.Items.Add(Weapon); end; |
Dependiendo de la dirección que indica el parámetro Dirección, crearemos el objeto con un sprite u otro y asignándole e un padre, en este caso es el que hemos pasado por el parámetro WeaponOwner.
Indicamos el punto de inicio, y la distancia máxima que será la mitad de ancho de la cámara. Cuando se alcanza esta distancia estamos fuera de la vista de la cámara.
Ajustamos su posición al origen. Cuando creamos el objeto este estaba en el origen de coordenadas. Ahora lo ponemos dónde nos interesa.
Ya solo nos queda asignar una velocidad, multiplicada por la dirección, así el arma irá hacía un lado u otro.
Ya solo nos queda añadir nuestra arma, al ViewPort, para que el motor gráfico se encargue de gestionarlo.
Actualizar el número de disparos
Para saber en cualquier momento el número de espadas que hay en cada momento, podemos buscar el número de objetos de tipo Tweapon que hay en el Viewport. Para ello vamos a crear un un procedimiento llamado UpdateNumberWeapons el cual lo llamaremos en el procedimiento FireWeapon.
El procedimiento UpdateNumberWeapons recorrerá los elemento que hay en Viewport y obtendrá el total de los que sean de tipo Tweapon actualizando la variable NumeroDisparos.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
procedure TViewMain.UpdateNumberWeapons; var I: integer; begin NumeroDisparos := 0; I := 0; while I < ViewPort1.Items.Count do begin if ViewPort1.Items[I].ClassType = TWeapon then begin if ViewPort1.Items[i].Exists = True then begin NumeroDisparos := NumeroDisparos + 1; end; end; I := I + 1; end; |
Modificaremos el procedimiento FireWeapon para que llame a este procedimiento, y dependiendo del valor de la variable NumeroDisparos, devolverá True o False.
|
1 2 3 4 5 6 7 8 9 10 11 |
function TViewMain.FireWeapon: boolean; begin Result := False; UpdateNumberWeapons; if (Container.Pressed[keySpace]) and (CanShot = True) and (NumeroDisparos <= 2) then begin NumeroDisparos := NumeroDisparos + 1; Result := True; StarTimerShot; end; end; |
Tienes todo el código completo en el Github.
Saludos
