Já faz um tempo desde o último artigo da série, um ano, para ser exato e eu gostaria de começar pedindo desculpas por esta ausência tão longa. Estive envolvido com vários projetos simultâneos nestes últimos 12 meses, ministrando muitas aulas e trabalhando com sistemas embarcados, o que foi muito enriquecedor mas acabou me afastando um pouco aqui do blog. Mea culpa, mea culpa...
Bom, explicações dadas, vamos ao que interessa: nosso game!
Arquitetura do motor do game. |
Ainda há bastante espaço para melhorá-la, mas temos, neste ponto, o suficiente para darmos uma polida nos gráficos do gameplay, criando a ilusão de um rastro de fumaça para os tiros e estilhaços das naves quando atingidas ou destruídas pelo jogador. Para implementar estes efeitos, vamos utilizar um sistema de partículas, simples mas muito poderoso!
Sistemas de Partículas
Um átomo e seus elétrons como um sistema de partículas |
A computação gráfica tomou a ideia da física emprestada e simplificou-a ao ponto de ser utilizável em sistemas de simulação de tempo real (como os jogos).
Para nosso uso, porém, vamos definir um sistema de partículas como sendo um conjunto de regras simples e claras que definem o comportamento de um conjunto de pontos no plano bidimensional. Sendo assim temos:
- Toda partícula nasce dentro de um emissor
- Toda partícula possui uma função que determina seu movimento
- Toda partícula possui um tempo de vida finito
- Cada partícula do sistema pode ser representada por um ponto ou uma imagem
Com estas regras, podemos imaginar que quando o míssil de nossa nave viaja pelo espaço em direção ao seu alvo, uma certa quantidade de fumaça irá sair pela sua base ou seja, a base do míssil irá emitir a fumaça. Achamos o emissor do nosso sistema.
Quando uma partícula de fumaça é emitida, ela nasce com uma velocidade inicial e um ângulo e seu movimento começa a sofrer mediata influência da força da gravidade. Depois de emitida, a partícula de fumaça começa a ser consumida pelo ar (ou atmosfera, se preferir) até o ponto em que deixará de existir. Definimos a função de movimento e vida de nossas partículas.
E, por fim, cada partícula do sistema será representada visualmente como um retângulo colorido que irá desaparecendo com tempo até sumir completamente. Definimos nossa representação.
De posse de todas as definições, podemos implementar o sistema. Vamos começar pela classe
TParticle
:TParticle = class(TGameObject) strict private fLife : real; fInitialLife : real; fAngle : real; fSpeed : real; fColor : TSDL_Color; fWidth : integer; fHeight : integer; fVelocity : TVector; function GetAlive: boolean; function GetAngleInRadians: real; inline; function GetVelocity: TVector; public constructor Create(aPosition: TVector; life, angle, speed: real); destructor Destroy; override; procedure Update(const deltaTime: real); override; property Life : real read fLife write fLife; property InitialLife: real read fInitialLife; property Angle : real read fAngle write fAngle; property Speed: real read fSpeed write fSpeed; property Color: TSDL_Color read fColor write fColor; property Width: integer read fWidth write fWidth; property Height: integer read fHeight write fHeight; property Alive: boolean read GetAlive; property AngleInRadians: real read GetAngleInRadians; property Velocity: TVector read GetVelocity; end; { TParticle } function TParticle.GetAngleInRadians: real; begin result := fAngle * (pi / 180); end; destructor TParticle.Destroy; begin fVelocity.Free; inherited; end; function TParticle.GetAlive: boolean; begin result := fLife > 0; end; function TParticle.GetVelocity: TVector; var radians: real; begin radians := GetAngleInRadians; fVelocity.x := fSpeed * cos(radians); fVelocity.y := fSpeed * sin(radians); result := fVelocity; end; constructor TParticle.Create(aPosition: TVector; life, angle, speed: real); begin inherited Create; Self.Position := aPosition; fLife:= life; fInitialLife := life; fAngle:= angle; fSpeed:= speed; fVelocity := TVector.Create; end; procedure TParticle.Update(const deltaTime: real); begin fLife := fLife - deltaTime; if (fLife > 0) then begin Position.X := Position.X + (fVelocity.X * deltaTime); Position.Y := Position.Y + (fVelocity.Y * deltaTime); end; end;
fLife := fLife - deltaTime
) e se moverá pelo espaço.Quem irá criar as partículas do sistema e atualizar a posição de cada uma será um emissor do tipo
TEmitter
:
TEmitter = class(TInterfacedObject, IUpdatable, IDrawable) strict private var fParticles : TParticleList; fKind : TEmitterKind; fBounds : TRect; fTimeAccum : Real; fSpawnFrequency : Real; fActive : Boolean; fGravity : TVector; fAngle : TRangeReal; fSpeed : TRangeReal; fWidth : TRangeReal; fHeight : TRangeReal; fLifeSpan : TRangeReal; fEmissionRate : Integer; fMaxCount : integer; fRenderer : PSDL_Renderer; fOnAllParticleDied : TNotifyEvent; procedure CreateNewParticle; procedure UpdateParticles(const deltaTime: real); procedure SpawnParticles(const deltaTime: real); protected procedure doOnAllParticleDied; virtual; public constructor Create(renderer: PSDL_Renderer); destructor Destroy; override; procedure Update(const deltaTime: real); procedure Draw; procedure Start; procedure Stop; property Particles : TParticleList read fParticles; property Kind : TEmitterKind read fKind write fKind; property Bounds : TRect read fBounds write fBounds; property Active : boolean read fActive; property Angle : TRangeReal read fAngle write fAngle; property Speed : TRangeReal read fSpeed write fSpeed; property Width : TRangeReal read fWidth write fWidth; property Height : TRangeReal read fHeight write fHeight; property LifeSpan : TRangeReal read fLifeSpan write fLifeSpan; property Gravity : TVector read fGravity write fGRavity; property MaxCount : integer read fMaxCount write fMaxCount; property EmissionRate : Integer read fEmissionRate write fEmissionRate; property OnAllParticleDied : TNotifyEvent read fOnAllParticleDied write fOnAllParticleDied; end; {implementation} procedure TEmitter.Draw; var i: integer; p: TParticle; r: TSDL_Rect; begin SDL_SetRenderDrawBlendMode(fRenderer, SDL_BLENDMODE_ADD); for i :=0 to fParticles.Count-1 do begin p := Particles[i]; if p.Alive then begin r.x := round(p.Position.X); r.y := round(p.Position.Y); r.w := round(p.Width); r.h := round(p.Height); SDL_SetRenderDrawColor(fRenderer, p.Color.r, p.Color.g, p.Color.b, p.Color.a); SDL_RenderFillRect(fRenderer, PSDL_Rect(@r)); end; end; end; procedure TEmitter.CreateNewParticle; var lParticle : TParticle; lPosition : TVector; lLife : Real; lAngle : Real; lSpeed : Real; lColor : TSDL_Color; begin lPosition := TVector.Create((fBounds.x) + Random(fBounds.w), (fBounds.y) + Random(fBounds.h)); lLife := fLifeSpan.Min + Random(Round(fLifeSpan.Max)); lAngle := fAngle.Min + (random * (fAngle.Max - fAngle.Min)); lSpeed := fSpeed.Min + (random * (fSpeed.Max - fSpeed.Min)); lColor.r := $FF; lColor.g := $FF; lColor.b := $AA; lColor.a := $FF; lParticle := TParticle.Create(lPosition, lLife, lAngle, lSpeed); lParticle.Color := lColor; lParticle.Width := Round(fWidth.Min + random * (fWidth.Max - fWidth.Min)); lParticle.Height := Round(fHeight.Min + random * (fHeight.Max - Height.Min)); fParticles.Add(lParticle); end; procedure TEmitter.Update(const deltaTime: real); begin fTimeAccum := fTimeAccum + deltaTime; if fActive then SpawnParticles(deltaTime); UpdateParticles(deltaTime); end; procedure TEmitter.UpdateParticles(const deltaTime: real); var i: integer; p: TParticle; color: TSDL_Color; listChanged: boolean; begin listChanged := false; for i:= Particles.Count-1 downto 0 do begin p := fParticles[i]; p.Life := p.Life - deltaTime; if p.Alive then begin p.Position.X := p.Position.X + (p.Velocity.X * deltaTime) + (fGravity.X * deltaTime); p.Position.Y := p.Position.Y + (p.Velocity.Y * deltaTime) + (fGravity.Y * deltaTime); color := p.Color; if p.InitialLife <= 0 then color.a := 0 else color.a := round(255 * (p.Life / p.InitialLife)); p.Color := color; end else begin fParticles.Remove(p); listChanged := true; end; end; if (listChanged and (Particles.Count = 0)) then doOnAllParticleDied; end;
Com
TEmitter
e TParticle
, estamos pontos para criar alguns efeitos. O emissor está criando partículas aleatoriamente e atualizando-as no método UpdateParticles
, onde a posição, o tempo de vida e a cor de cada partícula computado. Depois disto, o método Draw
desenha as partículas vivas de acordo com o que UpdateParticles
calculou.Fumaça e Explosões
Para criar efeitos utilizando as classes introduzidas acima, basta instanciar um emissor e ir alterando os parâmetros para conseguir o resultado desejado. Veja a imagem abaixo. Temos 6 emissores configurados de maneiras distintas. Nos 4 primeiros, há uma emissão contínua de partículas. Elas morrem e são recriadas em seguida em algum ponto dentro do emissor. Há também uma gravidade puxando-as para baixo e elas possuem um tempo de vida muito curto. Vamos 'anexar' um emissor deste tipo a cada míssil lançado para criar um rasto dinâmico de fumaça.
Efeitos criados modificando os parâmetros do emissor |
Agora veja os 2 últimos efeitos.
Eles são idênticos aos outros ecxeto pelo fato de suas partículas não serem afetadas pela gravidade e terem uma velocidade e um ângulo de movimento inicial aleatório. As partículas aqui são emitidas todas de uma vez e não renascem mais. Vamos instanciar uma delas quando um inimigo for destruído.
Para não ter que lembrar os parâmetros iniciais cada vez que quisermos um novo emissor, vamos criar uma classe auxiliar com dois métodos estáticos. Um para criar uma fumaça contínua e outro para criar os estilhaços.
TEmitterFactory = class class function NewSmokeOneShot: TEmitter; class function NewSmokeContinuous: TEmitter; end; { TEmitterFactory } class function TEmitterFactory.NewSmokeContinuous: TEmitter; var bounds : TSDL_Rect; begin result := NewSmokeOneShot; result.Kind := ekContinuous; result.LifeSpan.Min := 0.2; result.LifeSpan.Max := 0.5; result.MaxCount := 600; result.EmissionRate := 30; end; { implementation } class function TEmitterFactory.NewSmokeOneShot: TEmitter; begin result := TEmitter.Create(TEngine.GetInstance.Renderer); result.Kind := ekOneShot; result.Bounds.x := 300; result.Bounds.y := 200; result.Bounds.w := 5; result.Bounds.h := 10; result.Width.Min := 2; result.Width.Max := 2; result.Height.Min := 1; result.Height.Max := 2; result.LifeSpan.Min := 1; result.LifeSpan.Max := 2; result.Gravity.Y := 50; result.Gravity.X := 0; result.Angle.Min := 270-30; result.Angle.Max := 270+30; result.MaxCount := 10; end;
Agora, cara míssil (
TShot
) pode emitir um rastro de fumaça! Basta criarmos um emissor e alterarmos seu método Draw. Veja como ficou a classe depois das alterações:TShot = class ( TGameObject ) strict private fDirection : TShotDirection; fSpeed : real; fShowSmoke : boolean; fSmokeEmitter : TEmitter; fActive: boolean; procedure DrawShot; public constructor Create; override; destructor Destroy; override; procedure Draw; override; procedure Update(const deltaTime : real); override; function IsInsideScreen: boolean; procedure StopEmitSmoke; procedure StartEmitSmoke; property Active : boolean read fActive write fActive; property Direction : TShotDirection read fDirection write fDirection; property Speed : real read fSpeed write fSpeed; property ShowSmoke : boolean read fShowSmoke write fShowSmoke; end; { implementation } constructor TShot.Create; begin inherited; fSpeed := 300.0; fDirection := TShotDirection.Up; fVisible := true; fShowSmoke := false; fSmokeEmitter := TEmitterFactory.NewSmokeContinuous; fSmokeEmitter.OnAllParticleDied := doOnAllParticleDied; fActive := True; end; destructor TShot.Destroy; begin fSmokeEmitter.Free; inherited; end; procedure TShot.Draw; begin if fVisible then DrawShot; if fShowSmoke then fSmokeEmitter.Draw; end; procedure TShot.Update(const deltaTime: real); var insideScreen: boolean; begin if fActive then begin case fDirection of TShotDirection.Up : fPosition.Y := fPosition.Y - (fSpeed * deltaTime); TShotDirection.Down : fPosition.Y := fPosition.Y + (fSpeed * deltaTime); end; end; fSmokeEmitter.Bounds.X := Round(fPosition.X); fSmokeEmitter.Bounds.W := fSprite.CurrentFrame.Rect.w; fSmokeEmitter.Bounds.Y := Round(fPosition.Y + fSprite.CurrentFrame.Rect.h); fSmokeEmitter.Update(deltaTime); insideScreen := isInsideScreen; if fVisible then fVisible := insideScreen; if (not insideScreen and fSmokeEmitter.Active) then StopEmitSmoke; end; procedure TShot.StartEmitSmoke; begin fSmokeEmitter.Start; end; procedure TShot.StopEmitSmoke; begin fSmokeEmitter.Stop; end;
Veja como ficaram os mísseis depois da alteração. Bem mais interessante, não?
Veja que há um discreto rasto de fumaça saindo dos mísseis |
Agora, como um toque final, vamos melhorar o efeito de explosão das naves atingidas, criando um emissor do tipo "OneShot" no local da explosão.
Na cena do game play, vamos adicionar um novo método para instanciar nosso emissor:
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.MaxCount := Random(30) + 30; result.Angle.Min := 0; result.Angle.Max := 380; result.Gravity.X := 0; result.Gravity.Y := 0; result.Start; end;
E, finalmente, vamos invocá-la depois de criar a explosão:
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); 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); fSparks.Add(SpawnNewSparkAt(enemy)); 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;
Note que
fSparks
é uma nova variável privada, do tipo TEmitter
que deve ser adicionada à cena.Compare os resultados:
Versão final, com fumaça, explosão e fragmentos sendo emitidos |
Bacana, não?
Com estes detalhes implementados, nosso game está mais próximo de uma versão final. Vamos, no próximo artigo, criar um efeito de estrelas se movimentado no fundo e finalizar o cenário da tela de gameplay.
Abraços e até lá.