Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Criando um Game Completo - Parte 6.1

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.

Ajustes Visuais - Modulação de Cores


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 ( 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á-la

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 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
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.

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

Share the post

Criando um Game Completo - Parte 6.1

×

Subscribe to Computação Gráfica E Jogos Em Pascal

Get updates delivered right to your inbox!

Thank you for your subscription

×