Câmera Dinâmica e Salto de Sprite

O objetivo deste tutorial é revisar os principais conceitos necessários para programar jogos. Criaremos um ambiente virtual onde você controla um cão que anda em um cenário onde a câmera o acompanha dinamicamente. A imagem abaixo exemplifica a implementação que buscamos.

Você pode fazer o download dos arquivos de imagem usado neste tutorial clicando no link abaixo.

Vamos organizar nosso código dividindo-o em 4 seções.

  • Condições Iniciais (onde poderemos calibrar a dinâmica do jogo)
  • Funções (para reaproveitamento de código ou organização)
  • Início do Gameloop com Atualização de Variáveis
  • Final do Gameloop com update de sprites, desenho de imagens e update da janela

Obs: O paradigma de orientação a objetos será evitado para garantir que o código seja entendido por iniciantes, mas alguns comentários acerca deste paradigma serão realizados esporadicamente.

Como iremos desenhar a animação do cão quando ele está parado (fazendo pequenos movimentos para dar vida ao personagem), precisamos verificar as imagens usadas nessa animação. Chamamos esta imagem de sprite, e ela é um conjunto de frames colocados lado a lado como podemos observar na figura.

Avaliando a imagem do cão parado verificamos que este sprite possui 6 frames. Então já podemos iniciar nosso código.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posxDog = 100
posyDog = 320

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedRight.x = posxDog
dogStoppedRight.y = posyDog

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS

    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    dogStoppedRight.update()
    dogStoppedRight.draw()
    janela.update()

As linhas em destaque acima são as responsáveis por instanciarmos o objeto que representa o cão parado e virado para a direita. Vamos adicionar um novo sprite que representa o cão andando para a direita e vamos fazer o controle desse movimento através do teclado. Para isso vamos verificar a imagem do sprite do cão andando para a direita.

Novamente verificamos a presença de 6 frames neste sprite.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posxDog = 100
posyDog = 320
velxDog = 250

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedRight.x = posxDog
dogStoppedRight.y = posyDog

dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkRight.x = posxDog
dogWalkRight.y = posyDog

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS + DESENHO E ATUALIZAÇÃO
    cenario.draw()
    if teclado.key_pressed('RIGHT'):
        dogWalkRight.x += velxDog * janela.delta_time()
        dogStoppedRight.x += velxDog * janela.delta_time()
        dogWalkRight.update()
        dogWalkRight.draw()
    else:
        dogStoppedRight.update()
        dogStoppedRight.draw()

    janela.update()

Neste momento verificamos que há uma necessidade de fazer um controle sobre qual Sprite será desenhado de acordo com o que estiver sendo pressionado no teclado. Além disso, o cenário deve ser desenhado antes de todos os outros sprites para que não haja sobreposição. Se continuarmos implementando com essa estrutura o nosso código irá ficar um pouco bagunçado. Então vamos instanciar uma nova variável que será usada para armazenar o estado do cachorro e a partir desse estado iremos desenhar o cão no final do código. Além disso, em vez de atualizarmos o x e o y de cada sprite dentro dos condicionais, vamos atualizar as variáveis posx e posy e usar elas para alterar a posição de todos os sprites.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posxDog = 100
posyDog = 320
velxDog = 250
dogState = 'parado-direita'

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedRight.x = posxDog
dogStoppedRight.y = posyDog

dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkRight.x = posxDog
dogWalkRight.y = posyDog

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    if teclado.key_pressed('RIGHT'):
        dogState = 'andando-direita'
        posxDog += velxDog * janela.delta_time()
    else:
        dogState = 'parado-direita'

    # Movimento
    dogStoppedRight.x = posxDog
    dogWalkRight.x = posxDog

    # DESENHO E ATUALIZAÇÃO
    cenario.draw()

    if dogState == 'parado-direita':
        dogStoppedRight.update()
        dogStoppedRight.draw()
    elif dogState == 'andando-direita':
        dogWalkRight.update()
        dogWalkRight.draw()

    janela.update()

Agora temos nosso código mais bem organizado. Podemos criar duas funções para os blocos que estão marcados de amarelo para tornar nosso código principal mais enxuto, e cada vez que adicionarmos uma sprite nova fazemos sua movimentação e seu desenho dentro dessas duas funções. Assim, vamos usar a estrutura switch / case do python para tornar o código mais legível.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posxDog = 100
posyDog = 320
velxDog = 250
dogState = 'parado-direita'

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedRight.x = posxDog
dogStoppedRight.y = posyDog

dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkRight.x = posxDog
dogWalkRight.y = posyDog

def movimentoDog():
    dogStoppedRight.x = posxDog
    dogWalkRight.x = posxDog

def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    if teclado.key_pressed('RIGHT'):
        dogState = 'andando-direita'
        posxDog += velxDog * janela.delta_time()
    else:
        dogState = 'parado-direita'
    
    movimentoDog()
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    janela.update()


O Gameloop do código está bem mais enxuto e organizado. Isso é importante para continuarmos seguindo nossa programação sem nos perdermos no código. Além disso é possível verificar que como a posição do cão é atualizada antes de desenhar, não precisamos definir ela no início do código. Basta adicionar a definição da posição y dentro da função de movimento. Apagaremos então as linhas em vermelho e configuraremos os próximos sprites com apenas 2 linhas de código para cada sprite. Vamos fazer o cão de mover também para a esquerda instanciando mais 2 sprites. Além disso, vamos definir que se o cão está virado para a direita (andando ou parado) ele inicia o GameLoop parado para a direita, e se ele estiver virado para a esquerda (andando ou parado) ele inicia o GameLoop parado para a esquerda, e atualizamos este estado caso alguma tecla seja pressionada. Atualizamos também as funções com esses novos sprites.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posxDog = 100
posyDog = 320
velxDog = 250
dogState = 'parado-direita'

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)

dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)

dogStoppedLeft = Sprite("dogStoppedLeft.webp", 6)
dogStoppedLeft.set_sequence_time(0, 6, 400, True)

dogWalkLeft = Sprite("dogWalkLeft.webp", 6)
dogWalkLeft.set_sequence_time(0, 6, 100, True)


def movimentoDog():
    dogStoppedRight.x = posxDog
    dogStoppedRight.y = posyDog
    dogWalkRight.x = posxDog
    dogWalkRight.y = posyDog
    dogStoppedLeft.x = posxDog
    dogStoppedLeft.y = posyDog
    dogWalkLeft.x = posxDog
    dogWalkLeft.y = posyDog

def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()
        case 'parado-esquerda':
            dogStoppedLeft.update()
            dogStoppedLeft.draw()
        case 'andando-esquerda':
            dogWalkLeft.update()
            dogWalkLeft.draw()


while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    if dogState == 'andando-direita' or dogState == 'parado-direita':
        dogState = 'parado-direita'
    if dogState == 'andando-esquerda' or dogState == 'parado-esquerda':
        dogState = 'parado-esquerda'

    if teclado.key_pressed('RIGHT'):
        dogState = 'andando-direita'
        posxDog += velxDog * janela.delta_time()
    elif teclado.key_pressed('LEFT'):
        dogState = 'andando-esquerda'
        posxDog -= velxDog * janela.delta_time()
    
    movimentoDog()
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    janela.update()


Neste ponto é interessante fazer 2 comentários. Um diz respeito do uso do “elif” no código. Ele indica que há uma preferência do cão andar para a direita caso as teclas de direita e esquerda sejam pressionadas juntas. Assim, o cão irá se movimentar de fato para a direita. Caso tivéssemos usado apenas o “if” o estado do cão se atualizaria para “andando-esqueda” ao final desse conjunto de condicionais, mas a sua posição se manteria a mesma, dando a impressão que o cão está andando em uma esteira, sem sair do lugar.

O outro comentário diz respeito ao udo do delta_time(). Como usaremos essa função de maneira mais frequente no código, é interessante chamá-la uma única vez no início do loop, armazenando o valor do tempo em uma variável e usando esta variável quando necessário. Isso reduz o processamento durante a execução do jogo, melhorando a eficiência do código.

Salto

Para implementar o salto do cão precisamos fazer várias alterações no código, além de implementar equações da física de um corpo em queda livre (para fazermos o movimento parabólico da queda do cão.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posyInicial = 320
posxDog = 100
posyDog = posyInicial
velxDog = 250
dogState = 'parado-direita'
velInicialSalto = -400
velyDog = 0
grav = 700

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedLeft = Sprite("dogStoppedLeft.webp", 6)
dogStoppedLeft.set_sequence_time(0, 6, 400, True)
dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkLeft = Sprite("dogWalkLeft.webp", 6)
dogWalkLeft.set_sequence_time(0, 6, 100, True)

dogJumpRight = Sprite("dogJumpRight.webp", 5)
dogJumpRight.set_sequence_time(0, 4, 100, False)
dogJumpLeft = Sprite("dogJumpLeft.webp", 5)
dogJumpLeft.set_sequence_time(0, 4, 100, False)


def movimentoDog():
    dogStoppedRight.x = posxDog
    dogStoppedRight.y = posyDog
    dogStoppedLeft.x = posxDog
    dogStoppedLeft.y = posyDog
    dogWalkRight.x = posxDog
    dogWalkRight.y = posyDog
    dogWalkLeft.x = posxDog
    dogWalkLeft.y = posyDog

    dogJumpRight.x = posxDog
    dogJumpRight.y = posyDog
    dogJumpLeft.x = posxDog
    dogJumpLeft.y = posyDog


def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()
        case 'parado-esquerda':
            dogStoppedLeft.update()
            dogStoppedLeft.draw()
        case 'andando-esquerda':
            dogWalkLeft.update()
            dogWalkLeft.draw()

        case 'saltando-direita':
            dogJumpRight.update()
            dogJumpRight.draw()
        case 'saltando-esquerda':
            dogJumpLeft.update()
            dogJumpLeft.draw()


while True:
    deltatempo = janela.delta_time()
    # ATUALIZAÇÃO DE VARIÁVEIS
    if dogState == 'andando-direita' or dogState == 'parado-direita':
        dogState = 'parado-direita'
    if dogState == 'andando-esquerda' or dogState == 'parado-esquerda':
        dogState = 'parado-esquerda'

    if teclado.key_pressed('RIGHT'):
        posxDog += velxDog * deltatempo
        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-direita'
    elif teclado.key_pressed('LEFT'):
        posxDog -= velxDog * deltatempo
        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-esquerda'
    

    if teclado.key_pressed('SPACE'):
        if not dogState == 'saltando-direita' and not dogState == 'saltando-esquerda':
            velyDog = velInicialSalto # Apenas na primeira entrada do condicional que avalia a tecla "SPACE" pressionada
        if (dogState == 'parado-direita' or dogState == 'andando-direita'):
            dogState = 'saltando-direita'
        elif(dogState == 'parado-esquerda' or dogState == 'andando-esquerda'):
            dogState = 'saltando-esquerda'
    
    if dogState == 'saltando-direita' or dogState == 'saltando-esquerda':
        # Queda Livre
        posyDog += velyDog * deltatempo + (grav * (deltatempo ** 2) ) / 2
        velyDog += grav * deltatempo
    
    if posyDog > posyInicial:
        posyDog = posyInicial
        velyDog = 0
        if dogState == 'saltando-direita':
            dogState = 'parado-direita'
        elif dogState == 'saltando-esquerda':
            dogState = 'parado-esquerda'
    
    
    movimentoDog()
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    janela.update()

As primeiras linhas em amarelo dizem respeito a física do movimento do cão. Temos que definir uma posição y inicial que representa a altura que a imagem do cão fica quando ele está apoiado no chão, e definimos também uma velocidade com a qual ele inicia o o salto ( será um valor fixo e negativo pois os valores de y diminuem no sentido para cima). Também definimos uma velocidade vertical para o cachorro (que inicia com o valor que definimos para o início do salto e irá atualizar ao longo de sua queda livre), e um valor pra gravidade que pode ser calibrada posteriormente.

Instanciamos e configuramos os sprites de salto antes do GameLoop e definimos seu movimento e implementamos seu desenho dentro das funções específicas para estes fins que serão chamadas no GameLoop.

Agora precisamos fazer algumas alterações para entrarmos corretamente no condicional que altera o estado do cachorro quado as teclas “RIGHT” ou “LEFT” são pressionadas. Se o cão está pulando, continuamdo querendo que a posição horizontal do cão se altere, mas não queremos que a imagem desenhada da tela seja de um cachorro andando, já que ele está pulando.

Quando a tecla “SPACE” é pressionada, então queremos alterar a velocidade vertical do cão apenas na primeira entrada deste condicional (caso contrário o cão irá subir enquanto a tecla space estiver sendo pressionada). Assim, alteramos a velocidade apenas se o seu estado não for pulando. Alteramos então o estado do cão para pulando e executamos a queda livre sempre que ele estiver nesse estado.

O cão sai da queda livre quando ele atinge o chão, ou seja, quando atinge o y inicial. É importante reposicionar o cão corretamente pois ele pode atingir um y maior que o inicial, e resetamos também sua velocidade e estado.

Aqui temos que fazer três comentários. O primeiro diz respeito ao sprite do salto. Ele tem 5 frames, mas paramos no quarto frame. Isso foi feito porque esse é o frame em que o cão está voando no ar, e o último frame deve ser usado apenas para o pouso. Além disso, apenas o primeiro salto mostra o cão pegando impulso, e o restante apenas usa o quarto frame. O terceiro comentário é que o cão não está virando no ar quando mudamos a direção durante o salto. Vamos focar em resolver estes três problemas agora.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
posyInicial = 320
posxDog = 100
posyDog = posyInicial
velxDog = 250
dogState = 'parado-direita'
velInicialSalto = -400
velyDog = 0
grav = 700
disparaTemporizadorPouso = False
tempoPouso = 0

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedLeft = Sprite("dogStoppedLeft.webp", 6)
dogStoppedLeft.set_sequence_time(0, 6, 400, True)
dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkLeft = Sprite("dogWalkLeft.webp", 6)
dogWalkLeft.set_sequence_time(0, 6, 100, True)
dogJumpRight = Sprite("dogJumpRight.webp", 5)
dogJumpRight.set_sequence_time(0, 4, 100, False)
dogJumpLeft = Sprite("dogJumpLeft.webp", 5)
dogJumpLeft.set_sequence_time(0, 4, 100, False)


def movimentoDog():
    dogStoppedRight.x = posxDog
    dogStoppedRight.y = posyDog
    dogStoppedLeft.x = posxDog
    dogStoppedLeft.y = posyDog
    dogWalkRight.x = posxDog
    dogWalkRight.y = posyDog
    dogWalkLeft.x = posxDog
    dogWalkLeft.y = posyDog
    dogJumpRight.x = posxDog
    dogJumpRight.y = posyDog
    dogJumpLeft.x = posxDog
    dogJumpLeft.y = posyDog


def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()
        case 'parado-esquerda':
            dogStoppedLeft.update()
            dogStoppedLeft.draw()
        case 'andando-esquerda':
            dogWalkLeft.update()
            dogWalkLeft.draw()
        case 'saltando-direita':
            dogJumpRight.update()
            dogJumpRight.draw()
        case 'saltando-esquerda':
            dogJumpLeft.update()
            dogJumpLeft.draw()


while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    deltatempo = janela.delta_time()
    if dogState == 'andando-direita' or dogState == 'parado-direita':
        dogState = 'parado-direita'
    if dogState == 'andando-esquerda' or dogState == 'parado-esquerda':
        dogState = 'parado-esquerda'

    if teclado.key_pressed('RIGHT'):
        posxDog += velxDog * deltatempo
        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-direita'
        if dogState == 'saltando-esquerda':
            dogState = 'saltando-direita'
            dogJumpLeft.stop()
            dogJumpRight.set_sequence_time(3, 4, 200, False)

    elif teclado.key_pressed('LEFT'):
        posxDog -= velxDog * deltatempo
        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-esquerda'
        if dogState == 'saltando-direita':
            dogState = 'saltando-esquerda'
            dogJumpRight.stop()
            dogJumpLeft.set_sequence_time(3, 4, 200, False)
    

    if teclado.key_pressed('SPACE'):
        if not dogState == 'saltando-direita' and not dogState == 'saltando-esquerda':
            velyDog = velInicialSalto
        if (dogState == 'parado-direita' or dogState == 'andando-direita'):
            dogState = 'saltando-direita'
        elif(dogState == 'parado-esquerda' or dogState == 'andando-esquerda'):
            dogState = 'saltando-esquerda'
    
    if dogState == 'saltando-direita' or dogState == 'saltando-esquerda':
        # Queda Livre
        posyDog += velyDog * deltatempo + (grav * (deltatempo ** 2) ) / 2
        velyDog += grav * deltatempo
        dogJumpRight.play()
        dogJumpLeft.play()
    
    if posyDog > posyInicial:
        posyDog = posyInicial
        velyDog = 0
        dogJumpLeft.set_sequence_time(4, 5, 100, False)
        dogJumpRight.set_sequence_time(4, 5, 100, False)
        disparaTemporizadorPouso = True

    if disparaTemporizadorPouso:
        tempoPouso += deltatempo
        if tempoPouso > 0.1:
            tempoPouso = 0
            disparaTemporizadorPouso = False
            dogJumpLeft.set_sequence_time(0, 4, 100, False)
            dogJumpRight.set_sequence_time(0, 4, 100, False)
            if dogState == 'saltando-direita':
                dogState = 'parado-direita'
            if dogState == 'saltando-esquerda':
                dogState = 'parado-esquerda'
                
    
    movimentoDog()
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    janela.update()

O temporizador declarado no início do código irá disparar quando o cão voltar a tocar o solo após o salto. Neste momento o sprite de pouso será executado em um intervalo de tempo definido no final do código (0.1 s). Após esse tempo resetamos o sprite, o estado do cão, as variáveis do temporizador e o sprite de salto.

Dentro dos condicionais que avaliam a direção do movimento do cão adicionamos um condicional que permite que o sprite mude de direção durante o salto e evitamos que toda sequencia de pegar impulso seja executada novamente configurando o sprite no frame do vôo. Além disso, configuramos o sprite para realizar a animação do pulo novamente através da função “play()”. Isso é importante após reconfigurar os frames que precisam ser exibidos.

Câmera Dinâmica

Vamos impedir que o cão se mova e fazer o cenário se mover no sentido oposto para dar a impressão que a câmera está acompanhando o cachorro. Quando o cão chegar no final da janela, o cenário para de se mover e o cão se move pela janela.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
dogState = 'parado-direita'
posxDog = 100
posyInicial = 320
posyDog = posyInicial
velyDog = 0
disparaTemporizadorPouso = False
tempoPouso = 0
posxCamera = 0

# Calibração
velxDog = 250
velInicialSalto = -400
grav = 700

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedLeft = Sprite("dogStoppedLeft.webp", 6)
dogStoppedLeft.set_sequence_time(0, 6, 400, True)
dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkLeft = Sprite("dogWalkLeft.webp", 6)
dogWalkLeft.set_sequence_time(0, 6, 100, True)
dogJumpRight = Sprite("dogJumpRight.webp", 5)
dogJumpRight.set_sequence_time(0, 4, 100, False)
dogJumpLeft = Sprite("dogJumpLeft.webp", 5)
dogJumpLeft.set_sequence_time(0, 4, 100, False)

def movimentoCamera():
    cenario.x = posxCamera

def movimentoDog():
    dogStoppedRight.x = posxDog
    dogStoppedRight.y = posyDog
    dogStoppedLeft.x = posxDog
    dogStoppedLeft.y = posyDog
    dogWalkRight.x = posxDog
    dogWalkRight.y = posyDog
    dogWalkLeft.x = posxDog
    dogWalkLeft.y = posyDog
    dogJumpRight.x = posxDog
    dogJumpRight.y = posyDog
    dogJumpLeft.x = posxDog
    dogJumpLeft.y = posyDog


def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()
        case 'parado-esquerda':
            dogStoppedLeft.update()
            dogStoppedLeft.draw()
        case 'andando-esquerda':
            dogWalkLeft.update()
            dogWalkLeft.draw()
        case 'saltando-direita':
            dogJumpRight.update()
            dogJumpRight.draw()
        case 'saltando-esquerda':
            dogJumpLeft.update()
            dogJumpLeft.draw()

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    deltatempo = janela.delta_time()
    if dogState == 'andando-direita' or dogState == 'parado-direita':
        dogState = 'parado-direita'
    if dogState == 'andando-esquerda' or dogState == 'parado-esquerda':
        dogState = 'parado-esquerda'

    if teclado.key_pressed('RIGHT'):
        if posxCamera > janela.width - cenario.width:
            posxCamera -= velxDog * deltatempo
        else:
            posxDog += velxDog * deltatempo
        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-direita'
        if dogState == 'saltando-esquerda':
            dogState = 'saltando-direita'
            dogJumpLeft.stop()
            dogJumpRight.set_sequence_time(3, 4, 200, False)

    elif teclado.key_pressed('LEFT'):
        if posxCamera < 0:
            posxCamera += velxDog * deltatempo
        else:
            posxDog -= velxDog * deltatempo
        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-esquerda'
        if dogState == 'saltando-direita':
            dogState = 'saltando-esquerda'
            dogJumpRight.stop()
            dogJumpLeft.set_sequence_time(3, 4, 200, False)
    
    # Salto
    if teclado.key_pressed('SPACE'):
        if not dogState == 'saltando-direita' and not dogState == 'saltando-esquerda':
            velyDog = velInicialSalto
        if (dogState == 'parado-direita' or dogState == 'andando-direita'):
            dogState = 'saltando-direita'
        elif(dogState == 'parado-esquerda' or dogState == 'andando-esquerda'):
            dogState = 'saltando-esquerda'
    
    if dogState == 'saltando-direita' or dogState == 'saltando-esquerda':
        # Queda Livre
        posyDog += velyDog * deltatempo + (grav * (deltatempo ** 2) ) / 2
        velyDog += grav * deltatempo
        dogJumpRight.play()
        dogJumpLeft.play()
    
    if posyDog > posyInicial:
        posyDog = posyInicial
        velyDog = 0
        dogJumpLeft.set_sequence_time(4, 5, 100, False)
        dogJumpRight.set_sequence_time(4, 5, 100, False)
        disparaTemporizadorPouso = True

    if disparaTemporizadorPouso:
        tempoPouso += deltatempo
        if tempoPouso > 0.1:
            tempoPouso = 0
            disparaTemporizadorPouso = False
            dogJumpLeft.set_sequence_time(0, 4, 100, False)
            dogJumpRight.set_sequence_time(0, 4, 100, False)
            if dogState == 'saltando-direita':
                dogState = 'parado-direita'
            if dogState == 'saltando-esquerda':
                dogState = 'parado-esquerda'
                
            
    movimentoDog()
    movimentoCamera()
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    janela.update()

Agora temos um problema. Quando o cão anda até sair do cenário e pressionamos a tecla para ir para o lado oposto, o cão não retoma a sua posição inicial antes do cenário começar a andar novamente. Além disso temos que implementar melhorias no posicionamento da câmera para que seja enfatizado o que está na frente do cão.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
dogState = 'parado-direita'
posxDog = 100
posyInicial = 320
posyDog = posyInicial
velyDog = 0
disparaTemporizadorPouso = False
tempoPouso = 0
posxCamera = 0
AtivarPosCamDir = False
AtivarPosCamEsq = False

# Calibração
velxDog = 250
velInicialSalto = -400
grav = 700
velxPosCam = 1000

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedLeft = Sprite("dogStoppedLeft.webp", 6)
dogStoppedLeft.set_sequence_time(0, 6, 400, True)
dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkLeft = Sprite("dogWalkLeft.webp", 6)
dogWalkLeft.set_sequence_time(0, 6, 100, True)
dogJumpRight = Sprite("dogJumpRight.webp", 5)
dogJumpRight.set_sequence_time(0, 4, 100, False)
dogJumpLeft = Sprite("dogJumpLeft.webp", 5)
dogJumpLeft.set_sequence_time(0, 4, 100, False)

def movimentoCamera():
    cenario.x = posxCamera

def movimentoDog():
    dogStoppedRight.x = posxDog
    dogStoppedRight.y = posyDog
    dogStoppedLeft.x = posxDog
    dogStoppedLeft.y = posyDog
    dogWalkRight.x = posxDog
    dogWalkRight.y = posyDog
    dogWalkLeft.x = posxDog
    dogWalkLeft.y = posyDog
    dogJumpRight.x = posxDog
    dogJumpRight.y = posyDog
    dogJumpLeft.x = posxDog
    dogJumpLeft.y = posyDog


def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()
        case 'parado-esquerda':
            dogStoppedLeft.update()
            dogStoppedLeft.draw()
        case 'andando-esquerda':
            dogWalkLeft.update()
            dogWalkLeft.draw()
        case 'saltando-direita':
            dogJumpRight.update()
            dogJumpRight.draw()
        case 'saltando-esquerda':
            dogJumpLeft.update()
            dogJumpLeft.draw()

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    deltatempo = janela.delta_time()
    if dogState == 'andando-direita' or dogState == 'parado-direita':
        dogState = 'parado-direita'
    if dogState == 'andando-esquerda' or dogState == 'parado-esquerda':
        dogState = 'parado-esquerda'

    if teclado.key_pressed('RIGHT'):
        # Se o cão ta antes da posição 100px, coloca o cão na posição 100px
        if posxDog < 100:
            posxDog += velxDog * deltatempo
        # Se a câmera não chegou no fim, rola a câmera
        if posxDog >= 100 and posxCamera > janela.width - cenario.width:
            posxCamera -= velxDog * deltatempo
        # Se chegou no fim do cenário e o cão não saiu do cenário, o cão pode andar
        if posxCamera <= janela.width - cenario.width and posxDog < 700 - dogWalkRight.width:
            posxDog += velxDog * deltatempo

        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-direita'
        if dogState == 'saltando-esquerda':
            dogState = 'saltando-direita'
            dogJumpLeft.stop()
            dogJumpRight.set_sequence_time(3, 4, 200, False)

    elif teclado.key_pressed('LEFT'):
        # Se o cão ta depois de 500px, posiciona ele em 500px
        if posxDog > 500:
            posxDog -= velxDog * deltatempo
        # Se a câmera não cheogu no início do cenário, rola a câmera
        if posxDog <= 500 and posxCamera < 0:
            posxCamera += velxDog * deltatempo
        # Se chegou no fim do cenário e o cão não saiu do cenário, o cão pode andar
        if posxCamera >= 0 and posxDog > 0:
            posxDog -= velxDog * deltatempo

        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-esquerda'
        if dogState == 'saltando-direita':
            dogState = 'saltando-esquerda'
            dogJumpRight.stop()
            dogJumpLeft.set_sequence_time(3, 4, 200, False)
    
    # Salto
    if teclado.key_pressed('SPACE'):
        if not dogState == 'saltando-direita' and not dogState == 'saltando-esquerda':
            velyDog = velInicialSalto
        if (dogState == 'parado-direita' or dogState == 'andando-direita'):
            dogState = 'saltando-direita'
        elif(dogState == 'parado-esquerda' or dogState == 'andando-esquerda'):
            dogState = 'saltando-esquerda'
    
    if dogState == 'saltando-direita' or dogState == 'saltando-esquerda':
        # Queda Livre
        posyDog += velyDog * deltatempo + (grav * (deltatempo ** 2) ) / 2
        velyDog += grav * deltatempo
        dogJumpRight.play()
        dogJumpLeft.play()
    
    if posyDog > posyInicial:
        posyDog = posyInicial
        velyDog = 0
        dogJumpLeft.set_sequence_time(4, 5, 100, False)
        dogJumpRight.set_sequence_time(4, 5, 100, False)
        disparaTemporizadorPouso = True

    if disparaTemporizadorPouso:
        tempoPouso += deltatempo
        if tempoPouso > 0.1:
            tempoPouso = 0
            disparaTemporizadorPouso = False
            dogJumpLeft.set_sequence_time(0, 4, 100, False)
            dogJumpRight.set_sequence_time(0, 4, 100, False)
            if dogState == 'saltando-direita':
                dogState = 'parado-direita'
            if dogState == 'saltando-esquerda':
                dogState = 'parado-esquerda'


    # Movimentação da Câmera
    if (dogState == 'andando-direita' or dogState == 'parado-direita' or dogState == 'saltando-direita') and posxDog > 100 and posxCamera > janela.width - cenario.width:
        AtivarPosCamDir = True
    if AtivarPosCamDir:
        posxDog -= velxPosCam * deltatempo
        posxCamera -= velxPosCam * deltatempo
        if posxDog < 100 or posxCamera <= janela.width - cenario.width:
            AtivarPosCamDir = False

    if (dogState == 'andando-esquerda' or dogState == 'parado-esquerda' or dogState == 'saltando-esquerda') and posxDog < 500 and posxCamera < 0:
        AtivarPosCamEsq = True
    if AtivarPosCamEsq:
        posxDog += velxPosCam * deltatempo
        posxCamera += velxPosCam * deltatempo
        if posxDog > 500 or posxCamera >= 0:
            AtivarPosCamEsq = False
    
            
    movimentoDog()
    movimentoCamera() 
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    janela.update()


Neste código adicionamos o movimento dinâmico da câmera de acordo com como o cão está na tela. Se ele estiver virado para direita, mas posicionado muito à esquerda da tela (mais que 100px) e se ainda tem cenário para mostrar no lado direito, então a câmera se movimentará para mostrar esse cenário. O movimento da câmera cessa quando o cão atinge os 100px ou quando o cenário acaba. Caso a condição de parada seja de acordo com a posição do cachorro, reposicionamos ele na tela para evitar escorregamento.

Os condicionais que definem como a câmera e o cão devem se movimentar foi redefinida e destacada em amarelo. Os comentários explicam o que está sendo testado em cada um dos 3 condicionais.

A essa altura você deve estar se perguntando por que temos uma função separada com uma única linha para fazer o movimento do cenário? O objetivo de manter esta função é que podemos adicionar elementos ao cenário que estão em uma camada acima. Percebemos por exemplo que quando movemos o cão para a esquerda ele fica na frente da árvore, dando um efeito visual ruim. Vamos corrigir isso.

from PPlay.window import *
from PPlay.gameimage import *
from PPlay.keyboard import *
from PPlay.sprite import *

# DEFINIÇÕES INICIAIS
# Variáveis
dogState = 'parado-direita'
posxDog = 100
posyInicial = 320
posyDog = posyInicial
velyDog = 0
disparaTemporizadorPouso = False
tempoPouso = 0
posxCamera = 0
AtivarPosCamDir = False
AtivarPosCamEsq = False

# Calibração
velxDog = 250
velInicialSalto = -400
grav = 700
velxPosCam = 1000

# Objetos
janela = Window(700,500)
cenario = GameImage("cenario.webp")
arvore = GameImage("arvore.webp")
cobra = Sprite("snake-sprite.png", 4)
cobra.set_sequence_time(0, 4, 400, True)
cobra.y = 360
teclado = Keyboard()

dogStoppedRight = Sprite("dogStoppedRight.webp", 6)
dogStoppedRight.set_sequence_time(0, 6, 400, True)
dogStoppedLeft = Sprite("dogStoppedLeft.webp", 6)
dogStoppedLeft.set_sequence_time(0, 6, 400, True)
dogWalkRight = Sprite("dogWalkRight.webp", 6)
dogWalkRight.set_sequence_time(0, 6, 100, True)
dogWalkLeft = Sprite("dogWalkLeft.webp", 6)
dogWalkLeft.set_sequence_time(0, 6, 100, True)
dogJumpRight = Sprite("dogJumpRight.webp", 5)
dogJumpRight.set_sequence_time(0, 4, 100, False)
dogJumpLeft = Sprite("dogJumpLeft.webp", 5)
dogJumpLeft.set_sequence_time(0, 4, 100, False)
dogDieRight = Sprite("dogDieRight.webp", 7)
dogDieRight.set_sequence_time(0, 7, 100, False)
dogDieLeft = Sprite("dogDieLeft.webp", 7)
dogDieLeft.set_sequence_time(0, 7, 100, False)

def movimentoCamera():
    cenario.x = posxCamera
    arvore.x = posxCamera
    cobra.x = posxCamera + 800

def movimentoDog():
    dogStoppedRight.x = posxDog
    dogStoppedRight.y = posyDog
    dogStoppedLeft.x = posxDog
    dogStoppedLeft.y = posyDog
    dogWalkRight.x = posxDog
    dogWalkRight.y = posyDog
    dogWalkLeft.x = posxDog
    dogWalkLeft.y = posyDog
    dogJumpRight.x = posxDog
    dogJumpRight.y = posyDog
    dogJumpLeft.x = posxDog
    dogJumpLeft.y = posyDog
    dogDieRight.x = posxDog
    dogDieRight.y = posyDog
    dogDieLeft.x = posxDog
    dogDieLeft.y = posyDog

def desenhoDog():
    match dogState:
        case 'parado-direita':
            dogStoppedRight.update()
            dogStoppedRight.draw()
        case 'andando-direita':
            dogWalkRight.update()
            dogWalkRight.draw()
        case 'parado-esquerda':
            dogStoppedLeft.update()
            dogStoppedLeft.draw()
        case 'andando-esquerda':
            dogWalkLeft.update()
            dogWalkLeft.draw()
        case 'saltando-direita':
            dogJumpRight.update()
            dogJumpRight.draw()
        case 'saltando-esquerda':
            dogJumpLeft.update()
            dogJumpLeft.draw()
        case 'morrendo-direita':
            dogDieRight.update()
            dogDieRight.draw()
        case 'morrendo-esquerda':
            dogDieLeft.update()
            dogDieLeft.draw()

while True:
    # ATUALIZAÇÃO DE VARIÁVEIS
    deltatempo = janela.delta_time()
    if dogState == 'andando-direita' or dogState == 'parado-direita':
        dogState = 'parado-direita'
    if dogState == 'andando-esquerda' or dogState == 'parado-esquerda':
        dogState = 'parado-esquerda'

    if teclado.key_pressed('RIGHT') and not (dogState == 'morrendo-direita' or dogState == 'morrendo-esquerda'):
        if posxDog < 100:
            posxDog += velxDog * deltatempo
        if posxDog >= 100 and posxCamera > janela.width - cenario.width:
            posxCamera -= velxDog * deltatempo
        if posxCamera <= janela.width - cenario.width and posxDog < 700 - dogWalkRight.width:
            posxDog += velxDog * deltatempo

        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-direita'
        if dogState == 'saltando-esquerda':
            dogState = 'saltando-direita'
            dogJumpLeft.stop()
            dogJumpRight.set_sequence_time(3, 4, 200, False)

    elif teclado.key_pressed('LEFT') and not (dogState == 'morrendo-direita' or dogState == 'morrendo-esquerda'):
        if posxDog > 500:
            posxDog -= velxDog * deltatempo
        if posxDog <= 500 and posxCamera < 0:
            posxCamera += velxDog * deltatempo
        if posxCamera >= 0 and posxDog > 0:
            posxDog -= velxDog * deltatempo

        if not (dogState == 'saltando-direita' or dogState == 'saltando-esquerda'):
            dogState = 'andando-esquerda'
        if dogState == 'saltando-direita':
            dogState = 'saltando-esquerda'
            dogJumpRight.stop()
            dogJumpLeft.set_sequence_time(3, 4, 200, False)



    # Salto
    if teclado.key_pressed('SPACE'):
        if not dogState == 'saltando-direita' and not dogState == 'saltando-esquerda':
            velyDog = velInicialSalto
        if (dogState == 'parado-direita' or dogState == 'andando-direita'):
            dogState = 'saltando-direita'
        elif(dogState == 'parado-esquerda' or dogState == 'andando-esquerda'):
            dogState = 'saltando-esquerda'
    
    if dogState == 'saltando-direita' or dogState == 'saltando-esquerda' or dogState == 'morrendo-direita' or dogState == 'morrendo-esquerda':
        # Queda Livre
        posyDog += velyDog * deltatempo + (grav * (deltatempo ** 2) ) / 2
        velyDog += grav * deltatempo
        dogJumpRight.play()
        dogJumpLeft.play()
    
    if posyDog > posyInicial:
        posyDog = posyInicial
        velyDog = 0
        dogJumpLeft.set_sequence_time(4, 5, 100, False)
        dogJumpRight.set_sequence_time(4, 5, 100, False)
        disparaTemporizadorPouso = True

    if disparaTemporizadorPouso:
        tempoPouso += deltatempo
        if tempoPouso > 0.1:
            tempoPouso = 0
            disparaTemporizadorPouso = False
            dogJumpLeft.set_sequence_time(0, 4, 100, False)
            dogJumpRight.set_sequence_time(0, 4, 100, False)
            if dogState == 'saltando-direita':
                dogState = 'parado-direita'
            if dogState == 'saltando-esquerda':
                dogState = 'parado-esquerda'




    # Movimentação da Câmera
    if (dogState == 'andando-direita' or dogState == 'parado-direita' or dogState == 'saltando-direita') and posxDog > 100 and posxCamera > janela.width - cenario.width:
        AtivarPosCamDir = True
    if AtivarPosCamDir:
        posxDog -= velxPosCam * deltatempo
        posxCamera -= velxPosCam * deltatempo
        if posxDog < 100 or posxCamera <= janela.width - cenario.width:
            AtivarPosCamDir = False
            if posxDog < 100: # Evitar Escorregamento
                posxDog = 100
            

    if (dogState == 'andando-esquerda' or dogState == 'parado-esquerda' or dogState == 'saltando-esquerda') and posxDog < 500 and posxCamera < 0:
        AtivarPosCamEsq = True
    if AtivarPosCamEsq:
        posxDog += velxPosCam * deltatempo
        posxCamera += velxPosCam * deltatempo
        if posxDog > 500 or posxCamera >= 0:
            AtivarPosCamEsq = False
            if posxDog > 500: # Evitar Escorregamento
                posxDog = 500    

    if posxDog + dogStoppedRight.width - 30 > cobra.x and posxDog + 30 < cobra.x + cobra.width and posyDog + dogStoppedRight.height - 30 > cobra.y:
        if (dogState == 'andando-esquerda' or dogState == 'parado-esquerda' or dogState == 'saltando-esquerda'):
            dogState = 'morrendo-esquerda'
        if (dogState == 'andando-direita' or dogState == 'parado-direita' or dogState == 'saltando-direita'):
            dogState = 'morrendo-direita'
            
    movimentoDog()
    movimentoCamera() 
    
    # DESENHO E ATUALIZAÇÃO
    cenario.draw()
    desenhoDog()
    cobra.update()
    cobra.draw()
    arvore.draw()
    janela.update()


Além dessa vantagem, agora também podemos adicionar inimigos e fazer a movimentação deles junto com o cenário nesta função. Repare que nada impede que alem de se mover com o cenário, o inimigo também tenha um movimento próprio, bastando adicionar valores aos seus atributos x e y quando implementar sua movimentação.

No código adicionamos o sprite que faz o cão morrer e adicionamos uma cobra. Se o cão encostar na cobra ele morre, então definimos um retângulo em torno do cão que não pode sobrepor ao retângulo em torno da cobra. Colocamos ainda 30 px de tolerância pois os sprites possuem pixels transparentes, e utilizamos o sprite do cão parado como referência para o teste.

Ao tocar na cobra o sprite que mostra o cão morrendo é disparado porque mudamos o estado do cachorro. Agora fica mais fácil adicionar inimigos e elementos na tela que se movimentam junto com o cenário. Os próximos passos seria adicionar inimigos, colocá-los em um vetor para agilizar o processo de testar de o cão morreu, fazer uma tela de game-over, implementar um menu, etc. No entanto, o escopo deste tutorial já foi contemplado.

Cŕedito: Sergio Herman