Modelo do controle usado para os testes |
No último artigo, criamos uma classe para encapsular a lógica do game, carregamos algumas imagens para a memória e exibimos o primeiro inimigo na tela. Seguiremos, agora, com a animação dos sprites e a movimentação do jogador através do teclado ou do joystick.
Vamos lá!
Sprites e Animação (sprite based)
PSDL_Texture
dentro de TGame
e passamos o ponteiro da textura para cada sprite instanciado.Isto funciona, mas há um problema. O
PSDL_Texture
é implementado na biblioteca como um ponteiro não tipado ( void *
), logo não há como o sprite saber as dimensões da textura e acabamos enchendo o código de números mágicos quee precisamos eliminar. Começaremos criado uma classe com estas informações para representar uma textura e, em seguida vamos alterar o código de carga das imagens para devolver uma instância dessa nova classe.TSpriteKind = ( EnemyA = 0, EnemyB = 1, EnemyC = 2, EnemyD = 3, Player = 4, Bunker = 5, Garbage = 6, Explosion = 7, ShotA = 8, Leds = 9 ); (...) TTexture = class W : integer; H : integer; Data: PSDL_Texture; procedure Assign( pTexure: TTexture ); end; (...) procedure TTexture.Assign(pTexure: TTexture); begin self.W := pTexure.W; self.H := pTexure.H; self.Data := pTexure.Data; end; (...) TGame = class private fRunning : boolean; fWindow : PSDL_Window; fWindowSurface : PSDL_Surface; fRenderer : PSDL_Renderer; fTextures : array of TTexture; procedure LoadTextures; procedure FreeTextures; (...) function TGame.LoadPNGTexture( const fileName: string ): TTexture; const IMAGE_DIR = '.\assets\images\'; var temp : PSDL_Surface; begin result := TTexture.Create; result.W := 0; result.H := 0; result.Data := nil; try temp := IMG_Load( PAnsiChar( IMAGE_DIR + fileName ) ); if ( temp = nil ) then raise SDLException.Create( SDL_GetError ) else begin result.W := temp^.w; result.H := temp^.h; result.Data := SDL_CreateTextureFromSurface( fRenderer, temp ); if ( result.Data = nil ) then raise SDLImageException.Create( IMG_GetError ); end; finally SDL_FreeSurface( temp ); end; end; (...) procedure TGame.LoadTextures; begin SetLength(fTextures, Ord( High( TSpriteKind ) ) +1 ); fTextures[ ord(TSpriteKind.EnemyA) ] := LoadPNGTexture( 'enemy_a.png' ); fTextures[ ord(TSpriteKind.EnemyB) ] := LoadPNGTexture( 'enemy_b.png' ); fTextures[ ord(TSpriteKind.EnemyC) ] := LoadPNGTexture( 'enemy_c.png' ); fTextures[ ord(TSpriteKind.EnemyD) ] := LoadPNGTexture( 'enemy_d.png' ); fTextures[ ord(TSpriteKind.Player) ] := LoadPNGTexture( 'player.png' ); fTextures[ ord(TSpriteKind.Bunker) ] := LoadPNGTexture( 'bunker.png' ); fTextures[ ord(TSpriteKind.Garbage)] := LoadPNGTexture( 'garbage.png' ); fTextures[ ord(TSpriteKind.Explosion)] := LoadPNGTexture( 'explosion.png' ); fTextures[ ord(TSpriteKind.ShotA) ] := LoadPNGTexture( 'shot_a.png' ); fTextures[ ord(TSpriteKind.Leds) ] := LoadPNGTexture( 'leds.png' ); end; procedure TGame.FreeTextures; var i: integer; begin for i:= low(fTextures) to High(fTextures) do SDL_FreeSurface( fTextures[ i ].Data ); SetLength(fTextures, 0); end;
Veja que criamos mais uma entrada registro
TSpriteKind
para armazenar uma imagem a mais - um led que utilizaremos mais à frente. Seguimos com a definição TTexture
que, além de um ponteiro para a textura, possui variáveis para armazenar a as dimensões da imagem para qual TTexture.Data
aponta. Substituímos campo fSprites
por fTextures
, e os métodos LoadSprites
e FreeSprirtes
viraram, respectivamente, LoadTextures
e FreeTextures
. Verifique seu código e faça as alterações necessárias até conseguir compilar.Com estas alterações, preparamos o terreno para criar uma classe para nossos sprites.
Vamos definir um sprite como sendo uma imagem que contém todos os frames de uma animação perfeitamente alinhadas em sequência de exibição, formando uma grade regular indexada em
0
, exatamente como um array. Observe a imagem abaixo e veja como esperamos que os quadros estejam distribuídos pela imagem.No exemplo, temos uma imagem de
160px
de largura por uma altura H
qualquer. Nela existem 6
frames de 32px
cada, já que 192/32 = 6
, distribuídos em uma única linha. Os números no lado superior esquerdo são os índices dos frames dentroExemplo de sprite sheet contendo 6 frames alinhados em uma única linha |
Em nossa implementação, a imagem estará armazenada em um
TTexture
e cada frame será representado por um retângulo armazenado em um SDL_Rect
. Além disto, cada frame deverá saber quanto tempo ficará visível durante a execução da animação a qual pertence.TSpriteFrame = class public Rect : TSDL_Rect; TimeSpan : Cardinal; //em milisegundos end; TSpriteAnimationType = ( NoLoop, Circular ); TSprite = class strict private const DEFAULT_FRAME_DELAY = 500; //em milisegundos var fTexture : TTexture; fFrames : array of TSpriteFrame; fFrameIndex : integer; fAnimationType : TSpriteAnimationType; fTimeSinceIndexChange : real; function GetCurrentFrame: TSpriteFrame; function GetFrameIndex: integer; function GetFrames(index: integer): TSpriteFrame; procedure FreeFrames; procedure AdvanceFrames(count: integer); public constructor Create; destructor Destroy; override; procedure InitFrames( const pRows, pColumns : integer ); procedure Update(const deltaTime : real); property Texture: TTexture read fTexture write fTexture; property FrameIndex: integer read GetFrameIndex; property Frames[index: integer] : TSpriteFrame read GetFrames; property CurrentFrame : TSpriteFrame read GetCurrentFrame; property AnimationType : TSpriteAnimationType read fAnimationType write fAnimationType; end; (...) { TSprite Implementation } function TSprite.GetFrames(index: integer): TSpriteFrame; begin if ( index > Length(fFrames)-1 ) or ( index <; 0 ) then raise IndexOutOfBoundsException.Create(IntToStr(index)); result := fFrames[ index ]; end; function TSprite.GetFrameIndex: integer; begin result := fFrameIndex; end; function TSprite.GetCurrentFrame: TSpriteFrame; begin if ( (fFrameIndex < 0) or (fFrameIndex > High(fFrames)) ) then raise IndexOutOfBoundsException.Create('GetCurrentFrame'); result := fFrames[fFrameIndex]; end; procedure TSprite.FreeFrames; var i: integer; begin for i:= 0 to Length(fFrames)-1 do fFrames[i].Free; SetLength(fFrames, 0); end; constructor TSprite.Create; begin fTexture := TTexture.Create; fFrameIndex := 0; fAnimationType := TSpriteAnimationType.Circular; fTimeSinceIndexChange := 0.0; end; destructor TSprite.Destroy; begin FreeFrames; fTexture.Free; end; // inicializa os frames da animação, quebrando a textura em um padrão // de pRows x pColumns (linhas x colunas) procedure TSprite.InitFrames(const pRows, pColumns: integer); var lRow, lColumn, frameW, frameH, spriteCount, i : integer; begin spriteCount := pRows * pColumns; frameW := fTexture.W div pColumns; frameH := fTexture.H div pRows; FreeFrames; SetLength(fFrames, spriteCount); i:= 0; for lRow := 0 to pRows-1 do for lColumn :=0 to pColumns-1 do begin fFrames[ i ] := TSpriteFrame.Create; fFrames[ i ].TimeSpan := DEFAULT_FRAME_DELAY; fFrames[ i ].Rect.w := frameW; fFrames[ i ].Rect.h := frameH; fFrames[ i ].Rect.x := lColumn * frameW; fFrames[ i ].Rect.y := lRow * frameH; inc( i ); end; fFrameIndex := 0; inherited; end; //atualiza o estado da animação de acordo com a variação de tempo //desde a última chamada procedure TSprite.Update(const deltaTime: real); var frameCount : UInt32; elapsedMS : UInt32; begin elapsedMS := trunc(( fTimeSinceIndexChange + deltaTime ) * MSecsPerSec ); if ( elapsedMS >= fFrames[ fFrameIndex ].TimeSpan ) then begin frameCount := elapsedMS div fFrames[ fFrameIndex ].TimeSpan; AdvanceFrames( frameCount ); fTimeSinceIndexChange := 0; end else fTimeSinceIndexChange := fTimeSinceIndexChange + deltaTime; end; procedure TSprite.AdvanceFrames(count: integer); var framesLength : integer; begin framesLength := Length(fFrames); case fAnimationType of TSpriteAnimationType.NoLoop: begin inc(fFrameIndex, count); if fFrameIndex > framesLength-1 then fFrameIndex := framesLength-1 else if (fFrameIndex < 0) then fFrameIndex:= 0; end; TSpriteAnimationType.Circular : begin fFrameIndex := (fFrameIndex + count) mod framesLength; end; end; end;
Apesar do tamanho da classe, o código de
TSprite
é muito simples e dispensa maiores explicações exceto, talvez, pelo método: TSprite.Update
.
As animações, como sabemos, são implementadas em computação gráfica como a mudança de estado em função do tempo ou seja, para cada ponto no tempo, a contar do início do programa, alteramos a variáveis de todos os objetos para refletir seu estado naquele momento. Para que isto funcione, precisamos contar a passagem do tempo e atualizar nossos objetos periodicamente. Este é o intuito de
TSprite.Update
. Ele recebe em deltaTime
, a quantidade de tempo decorrida desde a última atualização. Este tempo é passado em frações de segundos.Para implementar a contagem do tempo, vamos alterar nosso game loop e adicionar um novo método em
TGame
.procedure TGame.Run; var deltaTime : real; thisTime, lastTime : UInt32; begin deltaTime := 0.0; thisTime := 0; lastTime := 0; fRunning := true; while fRunning do begin thisTime := SDL_GetTicks; deltaTime := real((thisTime - lastTime) / 1000); lastTime := thisTime; HandleEvents; Update( deltaTime ); Render; SDL_Delay(1); end; end; (...) procedure TGame.Update(const deltaTime : real ) ; var i: integer; begin for i:= 0 to High(fEnemies) do fEnemies[i].Update(deltaTime); end;
A função SDL_GetTicks fornece a chave para a lógica de contagem do tempo decorrido entre cada iteração do loop. Ela retorna a quantidade de milisegundos desde a inicialização do SDL. Com esta informação, calculamos a o intervalo
detaTime
com uma subtração ( thisTime - lastTime
) e dividimos o resultado por 1000
para converter o valor para a escala dos segundos. Feito isto, sabemos o quanto devemos avançar o estado dos objetos na linha do tempo e a função TGame.Update
é responsável por isto, iterando por todos os objetos do jogo e chamando a versão adequada de TGameObject.Update
para cada um ( TSprite.Update
, no caso dos sprites explicados nos parágrafos anteriores ).Ao final do loop, note a chamada de SDL_Delay(1). Ela faz com que o programa aguarde
1ms
antes de prosseguir para garantir que o intervalo entre as chamadas sempre seja maior 0ms
, evitando assim, uma divisão por zero no nosso cálculo e, de quebra, dando um tempinho para o processado, evitando um consumo desnecessariamente alto de cpu.Vamos seguir alterando as classes do game para que todo
TGameObject
tenha um método Update
, para que TEnemy
possua um Sprite
e para que TEnemyA
saiba desenhar a si mesmo como um sprite animado.TGameObject = class strict protected fRenderer : PSDL_Renderer; public Position : TPoint; constructor Create( const aRenderer: PSDL_Renderer ); virtual; procedure Update(const deltaTime : real ); virtual; abstract; procedure Draw; virtual; abstract; end; TEnemy = class( TGameObject ) private fSprite : TSprite; public HP : integer; constructor Create( const aRenderer: PSDL_Renderer ); override; destructor Destroy; override; procedure Update(const deltaTime : real); override; property Sprite: TSprite read fSprite; end; (...) { TEnemy Implementation } constructor TEnemy.Create(const aRenderer: PSDL_Renderer); begin inherited Create(aRenderer); fSprite := TSprite.Create; end; destructor TEnemy.Destroy; begin fSprite.Free; inherited Destroy; end; procedure TEnemy.Update(const deltaTime : real); begin Sprite.Update(deltaTime); end; (...) procedure TEnemyA.Draw; var source, destination : TSDL_Rect; begin if ( HP > 0 ) and ( fSprite.Texture.Data <> nil ) then begin source := Sprite.CurrentFrame.Rect; destination.x := Round(self.Position.X); destination.y := Round(self.Position.Y); destination.w := 16; destination.h := 16; SDL_RenderCopy( fRenderer, fSprite.Texture.Data, @source, @destination) ; end;Bingo!
Neste ponto o game deve estar executando sem erros e a animação dos inimigos sendo exibida. Caso tenha dificuldades para fazer o game compilar, faça o download do código-fonte e use-o como referência. É muito importante entender o que fizemos até aqui antes de seguir para a próxima seção.
Input de Teclado ( movendo e atirando )
ESC
para fechar a janela do game. Vamos agora refinar o método TGame.HandleEvents para movimentar o jogardor pela tela usando as setas do teclado ou as teclas A
e D
, numa versão simplificada do esquema de movimentação WASD
. Mas antes de seguir, precisamos de um objeto para representar o jogador, capaz de mover-se pela tela para a esquerda ou direita, em resposta aos inputs do jogador.Observe a listagem abaixo e, depois de estudá-la um pouco, implemente-a em seu projeto.
TGameObjectNotifyEvent = procedure(Sender: TGameObject) of object; TPlayerInput = ( Left, Right, Shot ); TPlayer = class( TGameObject ) private const DEFAULT_SPEED = 200.0; DEFAULT_COOLDOWN = 500; var fSpeed : real; fSprite : TSprite; fCooldown : integer; fCooldownCounter : integer; fInput : array[0..2] of boolean; fOnShotTriggered : TGameObjectNotifyEvent; function GetInput(index: integer): boolean; procedure SetInput(index: integer; AValue: boolean); public constructor Create(const aRenderer: PSDL_Renderer); override; destructor Destroy; override; procedure Draw; override; procedure Update(const deltaTime : real); override; property Input[index: integer] : boolean read GetInput write SetInput; property Sprite: TSprite read fSprite; property Speed : real read fSpeed write fSpeed; //em pixels por segund property Cooldown: integer read fCooldown write fCooldown; property CooldownCounter: integer read fCooldownCounter; property OnShotTriggered : TGameObjectNotifyEvent read fOnShotTriggered write fOnShotTriggered; end; (...) { TPlayer Implementation } { TPlayer } function TPlayer.GetInput(index: integer): boolean; begin if index > Length(fInput)-1 then raise IndexOutOfBoundsException.CreateFmt('TPlayer.GetInput(%d)', [index]); result := fInput[index]; end; procedure TPlayer.SetInput(index: integer; AValue: boolean); begin if index > Length(fInput)-1 then raise IndexOutOfBoundsException.CreateFmt('TPlayer.SetInput(%d)', [index]); fInput[index] := AValue; end; constructor TPlayer.Create(const aRenderer: PSDL_Renderer); var i : integer; begin inherited Create(aRenderer); fSprite := TSprite.Create; fSpeed := DEFAULT_SPEED; fCooldown := DEFAULT_COOLDOWN; fCooldownCounter:= 0; for i :=0 to High(fInput) do fInput[i] := false; end; destructor TPlayer.Destroy; begin fSprite.Free; inherited Destroy; end; procedure TPlayer.Draw; var source, destination : TSDL_Rect; begin if ( fSprite.Texture.Data <> nil ) then begin source := Sprite.CurrentFrame.Rect; destination.x := Round(self.Position.X); destination.y := Round(self.Position.Y); destination.w := 26; destination.h := 16; SDL_RenderCopy( fRenderer, fSprite.Texture.Data, @source, @destination) ; end; end; procedure TPlayer.Update(const deltaTime: real); begin if fInput[Ord(TPlayerInput.Left)] then Position.X:= Position.X - ( fSpeed * deltaTime ); if fInput[Ord(TPlayerInput.Right)] then Position.X:= Position.X + ( fSpeed * deltaTime ); if fCooldownCounter > 0 then dec(fCooldownCounter, round(deltaTime * MSecsPerSec)); if ( (fInput[Ord(TPlayerInput.Shot)]) and ( fCooldownCounter <= 0) ) then begin if Assigned(fOnShotTriggered) then fOnShotTriggered(self); fCooldownCounter := fCooldown; fInput[Ord(TPlayerInput.Shot)] := false; end; end;
Note que, por especializar
TGameObject
, TPlayer
se parece muito com TEnemy
, o que é ótimo pois nos ajuda a compreender seu código mais rapidamente. Veja também como TPlayer
sabe como mover-se dentro do método Update
: ele mantém, em um array, um valor booleano para cada entrada possível ( Left
, Right
e Shot
) e, caso estes indiquem que há um movimento a ser atualizado naquele instante, a posição é atualizada multiplicando a velocidade pela variação de tempo ( fSpeed * deltaTime
). Se um tiro deve ser disparado, um evento é chamado, delegando a responsabilidade para qualquer um que implementá-lo.Agora, podemos alterar
TGame.HandleEvents
para informar fPlayer
o estado dos inputs.procedure TGame.HandleEvents; var event : TSDL_Event; begin while SDL_PollEvent( @event ) = 1 do begin case event.type_ of SDL_QUITEV : fRunning := false; SDL_KEYDOWN : case event.key.keysym.sym of SDLK_ESCAPE: fRunning := false; SDLK_LEFT, SDLK_A : fPlayer.Input[Ord(TPlayerInput.Left)] := true; SDLK_RIGHT, SDLK_D : fPlayer.Input[Ord(TPlayerInput.Right)]:= true; end; SDL_KEYUP : case event.key.keysym.sym of SDLK_LEFT, SDLK_A : fPlayer.Input[Ord(TPlayerInput.Left)] := false; SDLK_RIGHT, SDLK_D : fPlayer.Input[Ord(TPlayerInput.Right)]:= false; SDLK_SPACE : fPlayer.Input[Ord(TPlayerInput.Shot)] := true end; end; end;
Antes de compilar, lembre-se declarar
fPlayer
dentro de TGame
e de realizar as devidas inicializações e finalizações.O que fizemos aqui foi capturar o pressionamento (
SDL_KEYDOWN
) e liberação (SDL_KEYUP
) das teclas que nos interessam e atualizar os flags de input do jogador para que quando a função TPlayer.Update
for chamada, ela saiba se deve mover o jogador ou criar um novo tiro.Execute o game e veja como agora é possível mover o jogador. Altere a variável
fSpeed
até ficar satisfeito com o resultado. O valor da constante DEFAULT_COOLDOWN
controla a frequência com que o jogador pode atirar.Perceba que a lógica de criação do disparo do jogador não foi criada. Ao invés disto, delegamos esta responsabilidade ao evento
OnShotTriggered
. A razão por trás desta escolha, é que tanto o jogador quando os inimigos podem efetuar disparos e, para manter o código organizado, vamos implementar a lógica de desenho e atualização dos projéteis em um único lugar. Uma classe única que manterá todos os projéteis do jogo, a TShotList
.TShotDirection = ( Up, Down ); TShot = class ( TGameObject ) private fDirection : TShotDirection; fSpeed : real; fVisible : boolean; fSprite : TSprite; public constructor Create(const aRenderer: PSDL_Renderer); override; destructor Destroy; override; procedure Draw; override; procedure Update(const deltaTime : real); override; property Sprite : TSprite read fSprite write fSprite; property Direction : TShotDirection read fDirection write fDirection; property Speed : real read fSpeed write fSpeed; property Visible : boolean read fVisible write fVisible; end; TShotList = class strict private const MAX_SHOTS = 64; var fShots : array[0..MAX_SHOTS-1] of TShot; function GetItems(index: integer): TShot; public constructor Create(const aRenderer: PSDL_Renderer; aTexture: TTexture); destructor Destroy; override; procedure Update(const deltaTime : real); procedure Draw; function NextIndexAvailable: integer; property Items[index: integer]: TShot read GetItems; default; end;
Definimos a classe
TShot
para representar um disparo. Cada disparo pode seguir, em linha reta, para cima ou para baixo (TShotDirection
) a uma velocidade constante (fSpeed
) e, sabe desenhar e atualizar a si mesma. Veja sua implementação.{ TShot } constructor TShot.Create(const aRenderer: PSDL_Renderer); begin inherited Create(aRenderer); fSpeed := 300.0; fDirection := TShotDirection.Up; fVisible := true; fSprite := TSprite.Create; end; destructor TShot.Destroy; begin fSprite.Free; inherited Destroy; end; procedure TShot.Draw; var source, destination : TSDL_Rect; begin if ( fSprite.Texture.Data <> nil ) and fVisible then begin source.x := 0; source.y := 0; source.w := 6; source.h := 12; destination.x := Round(self.Position.X); destination.y := Round(self.Position.Y); destination.w := 6; destination.h := 12; SDL_RenderCopy( fRenderer, fSprite.Texture.Data, @source, @destination) ; end; end; procedure TShot.Update(const deltaTime: real); begin if fVisible then begin case fDirection of TShotDirection.Up : Position.Y := Position.Y - (fSpeed * deltaTime ); TShotDirection.Down : Position.Y := Position.Y + (fSpeed * deltaTime ); end; fVisible := ( (Position.Y >= -(fSprite.Texture.H+1)) and (Position.Y < 600+1) ) and ( (Position.X >= -(fSprite.Texture.W+1)) and (Position.X < 800+1) ); end; end;
Mais uma vez, ela segue o padrão dos outros descendentes de
TGameObject
, encapsulando o conhecimento necessário para desenhar e atualizar a si mesmo. No final do método update, verificamos se depois de mover-se o projétil continua dentro da área visível do jogo e ajusta sua visibilidade de acordo.A implementação de
TShotList
, logo abaixo, também é simples. Mantemos um array de TShot
inicializado e vamos reutilizar as instâncias invisíveis para representar os tiros que forem necessários. Caso o seja solicitado um novo índice e não houver nenhum disponível, a função NextIndexAvailable
retorna -1, indicando neste instante, não há mais tiros disponíveis, efetivamente limitando a quantidade de projéteis que podem estar visíveis na tela em um dado instante do gameplay, baseando-se no valor da constante MAX_SHOTS
.{ TShotList } function TShotList.GetItems(index: integer): TShot; begin if index > High(fShots) then raise IndexOutOfBoundsException.CreateFmt('TShotList.GetItems(%d)', [index]); result := fShots[index]; end; constructor TShotList.Create(const aRenderer: PSDL_Renderer; aTexture: TTexture); var i : integer; begin for i:= 0 to MAX_SHOTS-1 do begin fShots[i] := TShot.Create( aRenderer ); fShots[i].Visible:= false; fShots[i].Position.X := - 100; fShots[i].Position.Y := - 100; fShots[i].Speed := 250; //pixels por segundo fShots[i].Sprite.Texture.Assign(aTexture); fShots[i].Sprite.InitFrames(1,1); end; end; destructor TShotList.Destroy; var i: integer; begin for i:= 0 to MAX_SHOTS-1 do; fShots[i].Free; end; procedure TShotList.Update(const deltaTime: real); var i: integer; begin for i:= 0 to MAX_SHOTS-1 do fShots[i].Update(deltaTime); inherited; end; procedure TShotList.Draw; var i: integer; begin for i:= 0 to MAX_SHOTS-1 do fShots[i].Draw; end; function TShotList.NextIndexAvailable: integer; var i: integer; begin result := -1; for i:= 0 to MAX_SHOTS-1 do if not fShots[i].Visible then begin result := i; break; end; end;
Com isto, finalizamos, por hora, os controles do jogador.
Ainda há o que melhorar. Veja, por exemplo, que é possível movê-lo para fora da área visível e que manter pressionada a barra de espaço sem mover a nave fará com que vários tiros consecutivos sejam criados. Esta é uma ótima oportunidade de exercita seu conhecimento até aqui e implementar estas correções por conta própria. Não se preocupe se tiver dificuldades, voltaremos a estas rotinas no futuro.
Suporte a Joysticks
Game rodando com suporte a joysticks |
Aviso dado, vamos em frente.
Para utilizar um joystick com a SDL é preciso inicializar o subsistema de controles através de uma chamada a SDL_Init, passado o flag
SDL_INIT_JOYSTICK
como parâmetro e depois adquirir um ponteiro para o dispositivo controlador através da função SDL_JoystickOpen.Começaremos, então, alterando o código de inicialização do game para incluir
SDL_INIT_JOYSTICK
nos flags.procedure TGame.Initialize; var flags, result: integer; begin if ( SDL_Init( SDL_INIT_VIDEO or SDL_INIT_TIMER or SDL_INIT_JOYSTICK ) <> 0 )then raise SDLException.Create(SDL_GetError); fWindow := SDL_CreateWindow( PAnsiChar( WINDOW_TITLE ), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 800, 600, SDL_WINDOW_SHOWN); if fWindow = nil then raise SDLException.Create(SDL_GetError); fWindowSurface := SDL_GetWindowSurface( fWindow ); if fWindowSurface = nil then raise SDLException.Create(SDL_GetError); fRenderer := SDL_CreateRenderer(fWindow, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC); if fRenderer = nil then raise SDLException.Create(SDL_GetError); flags := IMG_INIT_PNG; result := IMG_Init( flags ); if ( ( result and flags ) <> flags ) then raise SDLImageException.Create( IMG_GetError ); LoadTextures; CreateGameObjects; end;
Agora podemos abrir um joystick para leitura.
Por questão de segurança, vamos sempre checar se há um joystick disponível antes de tentar utilizá-lo e se não houver mais (um controle pode ser desconectado no meio do jogo, por exemplo) fechamos seu manipulador. Faremos isto em uma nova rotina que incluiremos no game loop.
Crie uma nova variável privada em
TGame
, chame-a de fJoystick
e defina seu tipo como PSDL_Joystick
, um ponteiro para um SDL_Joystick e implemente o código abaixo.procedure TGame.CheckDevices; var numJoysticks: SInt32; begin numJoysticks:= SDL_NumJoysticks; if (numJoysticks > 0) then begin if (fJoystick = nil) then begin fJoystick:= SDL_JoystickOpen(0); exit; end; end else if fJoystick <> nil then begin SDL_JoystickClose(fJoystick); fJoystick := nil; exit; end; end; (...) procedure TGame.Run; var deltaTime : real; thisTime, lastTime : UInt32; begin deltaTime := 0.0; thisTime := 0; lastTime := 0; fRunning := true; while fRunning do begin thisTime := SDL_GetTicks; deltaTime := real((thisTime - lastTime) / MSecsPerSec); lastTime := thisTime; CheckDevices; //atualizamos o status do joystick no começo de cada iteração HandleEvents; Update( deltaTime ); Render; SDL_Delay(1); end; end;
Com isto, é seguro plugar ou remover um joystick durante a partida. O jogo sempre saberá inicializar e liberar os recursos necessários em cada uma das situações.
O SDL notifica os eventos associados aos joysticks de maneira idêntica ao que faz com os eventos de teclado. Para cada evento ocorrido no dispositivo, a biblioteca adiciona uma mensagem na pilha e nós podemos capturá-las facilmente dentro de nosso
TGame.HandleEvents
. Observe os identificadores no padrão SDL_JOY*
no case principal da função.procedure TGame.HandleEvents; var event : TSDL_Event; begin while SDL_PollEvent( @event ) = 1 do begin case event.type_ of SDL_QUITEV : fRunning := false; SDL_KEYDOWN : case event.key.keysym.sym of SDLK_ESCAPE: fRunning := false; SDLK_LEFT, SDLK_A : fPlayer.Input[Ord(TPlayerInput.Left)] := true; SDLK_RIGHT, SDLK_D : fPlayer.Input[Ord(TPlayerInput.Right)]:= true; SDLK_SPACE : fPlayer.Input[Ord(TPlayerInput.Shot)] := true; end; SDL_KEYUP : case event.key.keysym.sym of SDLK_LEFT, SDLK_A : fPlayer.Input[Ord(TPlayerInput.Left)] := false; SDLK_RIGHT, SDLK_D : fPlayer.Input[Ord(TPlayerInput.Right)]:= false; SDLK_SPACE : fPlayer.Input[Ord(TPlayerInput.Shot)] := false; end; SDL_JOYAXISMOTION : case event.jaxis.axis of //X axis motion 0 : begin fPlayer.Input[Ord(TPlayerInput.Left)] := false; fPlayer.Input[Ord(TPlayerInput.Right)] := false; if event.jaxis.value > 0 then fPlayer.Input[Ord(TPlayerInput.Right)] := true else if event.jaxis.value < 0 then fPlayer.Input[Ord(TPlayerInput.Left)] := true end; end; SDL_JOYBUTTONUP : case event.jbutton.button of 0, 1, 2, 3 : begin fPlayer.Input[Ord(TPlayerInput.Shot)] := false; end; end; SDL_JOYBUTTONDOWN : case event.jbutton.button of 0, 1, 2, 3 : begin fPlayer.Input[Ord(TPlayerInput.Shot)] := true; end; end; end; end; end;
Bingo! Plugue o joystick e verá que pode controlar a nave com os controles direcionais e atirar utilizando os botões.
O eventos de navegação direcional horizontal são reportados como um movimento no eixo X utilizando a cosnstante
SDL_JOYAXISMOTION
. Se o valor lido for maior que 0, significa que houve um pressionamento na seta para a direita, se for menor que 0, a seta esquerda do joystick foi pressionada.A mesma lógica é utilizada para reportar o pressionamento (
SDL_JOYBUTTONUP
) e liberação (SDL_JOYBUTTONDOWN
) dos botões. Como existem inúmeros modelos de controladores de jogo, cada um com um número de diferente de botões disponíveis, capturamos os eventos dos 4 primeiros para garantir que você possa atirar com o botão de ação que achar mais confortável.Como um bônus e para ajudar a debugar o código referente aos inputs de josysticks, criamos uma função que vai desenhar um led vermelho na tela quando não houver nenhum joystics disponível e um led verde quando o SDL detectar a presença de pelo menos 1 controle.
procedure TGame.DrawDebugInfo; var source, dest : TSDL_Rect; begin //desenha o led do sensor do controle source.y := 0; source.w := 16; source.h := 16; if SDL_NumJoysticks > 0 then source.x := 0 else source.x := 16; dest.x := 10; dest.y := 10; dest.w := 16; dest.h := 16; SDL_RenderCopy( fRenderer, fTextures[Ord(TSpriteKind.Leds)].Data, @source, @dest); end; (...) procedure TGame.Render; begin SDL_SetRenderDrawColor( fRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE ); SDL_RenderClear( fRenderer ); DrawGameObjects; DrawDebugInfo; //vamos desenhar o "sensor" de joystick SDL_RenderPresent( fRenderer ); fFrameCounter.Increment; end;
Muito código foi escrito neste artigo, mas se você acompanhou com cuidado os comentários e explicações, deverá conseguir entender tudo o que fizemos. Se tiver dúvidas, críticas ou sugestões, sinta-se à vontade para utilizar a seção de comentários logo abaixo e se você chegou até aqui, meus parabéns pela dedicação! Posso garantir que a maior parte do "trabalho" sujo já foi feita e que o jogo tomará forma num ritmo cada vez maior nos próximos artigos da série.
Mais uma vez, obrigado pelo seu tempo e até a próxima!