Neste post, vamos complementar as melhorias introduzidas no texto anterior e criar um efeito de estrelas para deixar o plano de fundo do jogo mais interessante. A imagem ao lado mostra como ficará o visual das animações na cena do gameplay no final do processo.
Ajustes Visuais - Modulação de Cores
Temos 3 tipos de inimigos na cena. O primeiro, mais fraco (
TEnemyA
), posicionado na primeira linha de tiro, só suporta um único tiro antes de ser destruído. O segundo ( TEnemyB
), posicionado na linha central da formação inimiga, suporta 2 impactos antes de morrer e fica vermelho quando atingido. E, por fim, temos o inimigo posicionado nas linhas do topo ( TEnemyC
) que, tendo 3 pontos de vida, suporta dois hits antes de explodir, mudando de cor de acordo com a vida restante.Esta lógica está implementada, de maneira global, em
TEnemy.Draw
. Veja:procedure TEnemy.Draw; var source, destination : TSDL_Rect; begin source := Sprite.CurrentFrame.Rect; destination := Sprite.CurrentFrame.GetPositionedRect(self.Position); SDL_SetTextureColorMod( fSprite.Texture.Data, 255, 255, 255); if ( HP > 0 ) and ( fSprite.Texture.Data <> nil ) then begin if self is TEnemyB then case HP of 1 : SDL_SetTextureColorMod( fSprite.Texture.Data, 255, 0, 0); end; if self is TEnemyC then case HP of 2 : SDL_SetTextureColorMod( fSprite.Texture.Data, 255, 0, 0); 1 : SDL_SetTextureColorMod( fSprite.Texture.Data, 200, 0, 0); end; SDL_RenderCopy( fRenderer, fSprite.Texture.Data, @source, @destination) ; if fDrawMode = TDrawMode.Debug then begin SDL_SetRenderDrawColor( fRenderer, 0, 255, 0, 255 ); SDL_RenderDrawRect( fRenderer, @destination ); end; end; end;
Isto tem funcionado bem. Chamamos
SDL_SetTextureColorMod
, para modular a cor da textura do inimigo, fazendo com que ele seja renderizado com as cores moduladas para uma cor diferente da original dependo do tipo e da quantidade de vida de cada inimigo. Como dito, isto funciona, mas por estar hardcoded dentro desta função, a rotina que irá instanciar as partículas durante a explosão não tem como saber a cor do inimigo para copiá-laVamos usar um pouco de orientação à objetos ao nosso favor para resolver a questão.
Primeiro, vamos introduzir um método virtual na superclasse
TEnemy
para devolver a cor de modulação. Depois, cada descendente vai implementar sua própria versão, sobrescrevendo o método e, por fim, vamos publicar o método como uma propriedade na classe base. Veja:TEnemy = class( TGameObject ) protected function GetColorModulation: TSDL_Color; virtual; public property ColorModulation : TSDL_Color read GetColorModulation; end; (* TEnemyA vai utilizar versão default do método GetColorModulation então não é preciso alterá-la *) class( TEnemyB ) protected // sobrescreveremos a função na classe base function GetColorModulation: TSDL_Color; override; end; TEnemyC = class( TEnemy ) protected // sobrescreveremos a função na classe base function GetColorModulation: TSDL_Color; override; end; { implementation } function TEnemy.GetColorModulation: TSDL_Color; begin result.r := 255; result.g := 255; result.b := 255; result.a := 255; end; {...} function TEnemyB.GetColorModulation: TSDL_Color; begin result.g := 0; result.b := 0; case HP of 1,0 : result.r := 200; else result := inherited GetColorModulation; end; end; {...} function TEnemyC.GetColorModulation: TSDL_Color; begin result.g := 0; result.b := 0; case HP of 2 : result.r := 200; 1,0 : result.r := 255; else result := inherited GetColorModulation; end; end;
Com estas alterações, podemos reescrever o método
TEnemy.Draw
, simplificando-o.procedure TEnemy.Draw; var source, destination : TSDL_Rect; begin source := Sprite.CurrentFrame.Rect; destination := Sprite.CurrentFrame.GetPositionedRect(self.Position); SDL_SetTextureColorMod( fSprite.Texture.Data, 255, 255, 255); if ( HP > 0 ) and ( fSprite.Texture.Data <> nil ) then begin SDL_SetTextureColorMod( fSprite.Texture.Data, Self.ColorModulation.r, Self.ColorModulation.g, Self.ColorModulation.b); SDL_RenderCopy( fRenderer, fSprite.Texture.Data, @source, @destination) ; if fDrawMode = TDrawMode.Debug then begin SDL_SetRenderDrawColor( fRenderer, 0, 255, 0, 255 ); SDL_RenderDrawRect( fRenderer, @destination ); end; end; end;
Agora que cada inimigo sabe informar os parâmetros da sua modulação de cor, podemos utilizar esta informação na hora de instanciar um novo emissor de partículas para os detritos explosão. Na unit que define a cena (
scnGameplay.pas
), vamos alterar o método SpawnNewSparkAt
function TGamePlayScene.SpawnNewSparkAt(enemy: TEnemy): TEmitter; begin result := TEmitterFactory.NewSmokeOneShot; result.Bounds.X := round((enemy.Position.X)); result.Bounds.Y := round(enemy.Position.Y); result.Bounds.W := round(enemy.SpriteRect.w); result.Bounds.H := round(enemy.SpriteRect.h); result.Angle.Min := 0; result.Angle.Max := 380; result.Gravity.X := 0; result.Gravity.Y := 5; // quanto mais dano, maior a quantidade de detritos case enemy.HP of 0 : result.MaxCount := RandomRange(60, 80); 1 : result.MaxCount := RandomRange(8, 20); 2 : result.MaxCount := RandomRange(3, 8); end; // dependendo do inimigo, a área de detritos é maior ou menor if enemy is TEnemyB then result.MaxCount := result.MaxCount + 20 else if enemy is TEnemyC then begin result.MaxCount := result.MaxCount + 50; result.Bounds.X := result.Bounds.X - 5; result.Bounds.Y := result.Bounds.Y + 5; result.Bounds.W := result.Bounds.W + 5; result.Bounds.H := result.Bounds.H + 5; end; result.Color := enemy.ColorModulation; result.Start; end;
Perfeito! Os detritos agora serão criados com a cor do inimigo atingido.
Uma última melhoria que podemos implementar neste sistema é fazer com que detrito sejam lançados ao espaço cada vez que um inimigo for atingido e não somente no momento de sua morte e explosão. Para isto, devemos alterar o método
doOnShotCollided
da cena do gameplay.procedure TGamePlayScene.doOnShotCollided(Sender, Suspect: TGameObject; var StopChecking: boolean); var shot : TShot; enemy : TEnemy; explostion : TExplosion; engine : TEngine; smoke : TEmitter; begin engine := TEngine.GetInstance; if ( Sender is TShot ) then begin shot := TShot(Sender); if (Suspect is TEnemy) and (TEnemy(Suspect).HP > 0) then begin enemy := TEnemy(Suspect); enemy.Hit( 1 ); engine.Sounds.Play(sndEnemyHit); { vamos criar um emissor a cada hit no inimigo! } fSparks.Add(SpawnNewSparkAt(enemy)); if enemy.Alive then fPlayer.Score := fPlayer.Score + 10 else begin fPlayer.Score := fPlayer.Score + 100; explostion := TExplosion.Create(); explostion.Sprite.Texture.Assign(engine.Textures[Ord(TSpriteKind.Explosion)]); explostion.Sprite.InitFrames(1,1); explostion.Position.Assign(enemy.Position); fExplosions.Add(explostion); end; shot.Visible := false; shot.Active := false; shot.StopEmitSmoke; StopChecking := true; exit; end; if ( Suspect is TPlayer ) then begin fPlayer.Hit( 1 ); explostion := TExplosion.Create; explostion.Sprite.Texture.Assign(engine.Textures[Ord(TSpriteKind.Explosion)]); explostion.Sprite.InitFrames(1,1); explostion.Position.Assign(Suspect.Position); fExplosions.Add( explostion ); engine.Sounds.Play( sndEnemyHit ); fShots.Remove( shot ); end; end; end;
Veja como ficou.
Bem melhor, não?
Partículas sendo criadas, a cada hit, em função da cor e do tipo do inimigo atingido |
Ajustes Visuais - Background
O toque final para a cena de gameplay ficar digna de ser vista, é dar-lhe um plano de fundo.
Como a ação ocorre no espaço, nada mais natural do que criarmos um fundo estrelado. Para tanto vamos definir uma classe
TStar
e uma classe TStarField
que irá gerenciar (manter, desenhar e atualizar) um array de estrelas;type TStar = class private fPosition : TVector3D; fRadius : Real; fLife : Real; fStartLife: Real; public constructor Create; destructor Destroy; property Position : TVector3D read fPosition; property Radius : Real read fRadius write fRadius; property Life : Real read fLife write fLife; property StartLife: Real read fStartLife write fStartLife; end; TStarField = class( TInterfacedObject, IUpdatable, IDrawable ) private const STARS_COUNT = 200; var fStars : array[0..STARS_COUNT] of TStar; procedure RandomizeStarts; procedure SpawnNewStar(i: integer); public constructor Create; destructor Destroy; procedure Update(const deltaTime : real ); procedure Draw; end;
Cada estrela terá uma posição no plano 2d, mas vamos simular 3 camadas de estrelas utilizando o eixo Z, por isto
fPosition
é um TVector3D
. Cada estrela nascerá em um ponto aleatório da tela, com um tempo de vida aleatório e irá se apagando, lentamente, até sumir para, em seguida, surgir em um novo ponto.O código abaixo mostra a implementação destas idéias em TStarField. Se você vem acompanhando os textos da série, deve ser fácil entendê-lo. O último parâmetro do SDL_SetRenderDrawColor dentro de
TStarField.Draw
, entretanto, merece um explicação. Ele é o canal alpha da cor da estrela e está sendo desenhada e, para que o fundo não brigue os os objetos do primeiro plano, definimos que o valor máximo do alpha de uma estrela será de somente 150 e que ele irá diminuir de acordo com sua vida, ficando totalmente invisível quando a estrela desaparecer.constructor TStarField.Create; var i : integer; begin inherited; for i := 0 to Pred(STARS_COUNT) do fStars[i] := TStar.Create; RandomizeStarts; end; destructor TStarField.Destroy; var i : integer; begin for i := 0 to Pred(STARS_COUNT) do begin fStars[i].Free; fStars[i] := nil; end; end; procedure TStarField.Draw; const LayerColors: array[0..2,0..2] of byte = ( ($99,$99,$99), (111,100,255), (6,00,171)); var i : integer; rect : TSDL_Rect; renderer: PSDL_Renderer; begin renderer := TEngine.GetInstance.Renderer; for i:=0 to Pred(STARS_COUNT) do begin rect.x := trunc( fStars[i].Position.X - fStars[i].Radius); rect.y := trunc( fStars[i].Position.Y - fStars[i].Radius); rect.w := trunc(2 * fStars[i].Radius); rect.h := rect.w; SDL_SetRenderDrawColor(renderer, LayerColors[Trunc(fStars[i].Position.Z), 0], LayerColors[Trunc(fStars[i].Position.Z), 1], LayerColors[Trunc(fStars[i].Position.Z), 2], Byte( Round(150* (fStars[i].Life / fStars[i].StartLife)))); SDL_RenderFillRect(renderer, @rect); end; end; procedure TStarField.RandomizeStarts; var i : integer; begin for i := 0 to Pred(STARS_COUNT) do SpawnNewStar(i); end; procedure TStarField.SpawnNewStar(i: integer); begin fStars[i].Position.X := RandomRange(-25, TEngine.GetInstance.Window.w+25); fStars[i].Position.Y := RandomRange(-25, TEngine.GetInstance.Window.h+25); fStars[i].Position.Z := RandomRange(0, 2); fStars[i].Radius := RandomRange(1, 4) / 2; fStars[i].StartLife := RandomRange(1000, 8000); fStars[i].Life := fStars[i].StartLife; end; procedure TStarField.Update(const deltaTime: real); var i : integer; begin for i := 0 to Pred(STARS_COUNT) do begin if fStars[i].Life > 0 then fStars[i].Life := fStars[i].Life - (1000*deltaTime) else SpawnNewStar(i); end; end;
Projeto compilando no Delphi XE3 |
Veja ao lado um screenshot do projeto rodando com as modificações deste post, a partir da ide do Delphi e note como, mesmo com todas as partículas e processamento adicional que inserimos, a quantidade de frames por segundo continua alta (entre 250 e 260 na minha máquina). Se quiser conferir a performance, baixe o executável para windows e veja como ele se comporta em sua máquina.
Neste ponto, nosso projeto está quase concluído. Vamos partir, no próximo post, para as finalizações. Concluiremos a tela inicial, a tela de game over e iniciaremos o port e os testes do game para Linux (utilizaremos o Ubuntu).
Abraço e obrigado a todos que chegaram conosco até aqui!
Até a próxima.