Bem vindo à parte 6.1 do nosso mini curso Criando um Game Completo, onde criamos uma versão do clássico Space Invaders compatível com Windows e Linux, utilizando aceleração de hardware para gráficos 2D, suporte a joysticks e uma tabela de scores online! Tudo isto compilando em Delphi e Lazarus.
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.
A primeira coisa que podemos melhorar no sistema de partículas, é fazer com que elas sejam criadas com as mesmas cores do inimigo que acabou de ser atingido, tornando assim, o efeito mais convincente (até agora todas as partículas estão sendo criadas com a cor default).
Temos 3 tipos de inimigos na cena. O primeiro, mais fraco (
Esta lógica está implementada, de maneira global, em
Isto tem funcionado bem. Chamamos
Vamos usar um pouco de orientação à objetos ao nosso favor para resolver a questão.
Primeiro, vamos introduzir um método virtual na superclasse
Com estas alterações, podemos reescrever o método
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 (
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
Veja como ficou.
Bem melhor, não?
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
Cada estrela terá uma posição no plano 2d, mas vamos simular 3 camadas de estrelas utilizando o eixo Z, por isto
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
Pronto! Nossa tela de gameplay está concluída.
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.
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.
Links
- Código fonte - GitHub
- Código fonte - Download direto
- Executável - Win32
This post first appeared on Computação Gráfica E Jogos Em Pascal, please read the originial post: here