Tutorial Rápido

Exemplo básico

Este tutorial explica como utilizar a FGAme para a criação de jogos ou simulações de física simples. A FGAme é um motor de jogos com ênfase na simulação de física. Todos os objetos, portanto, possuem propriedades físicas bem definidas como massa, momento de inércia, velocidade, etc. A simulação da física é feita, em grande parte, de modo automático.

O primeiro passo é definir o palco que os objetos habitarão. Isto pode ser feito instanciando um objeto da classe World.

>>> world = World()

A partir daí, podemos criar objetos e inserí-los na simulação

>>> obj1 = Circle(50)
>>> obj2 = Circle(50, color='red')
>>> world.add([obj1, obj2])

Para modificar as propriedades físicas dos objetos basta modificar diretamente os atributos correspondentes. Uma lista completa de atributos pode ser encontrada no módulo FGAme.objects.

>>> obj1.mass = 10 >>> obj2.mass = 20 >>> obj1.pos = (150, 50) >>>
obj1.vel = (-100, 0)

As variáveis dinâmicas podem ser modificadas diretamente, mas sempre que possível, devemos utilizar os métodos que realizam os deslocamentos relativos (ex.: .imove(), .boost(), etc). Estes métodos geralmente são mais eficientes e seguros.

Aplicamos uma operação de .imove() para movê-lo com relação à posição

anterior. Veja como fica a posição final do objeto.

>>> obj1.imove(150, 0)  # deslocamento com relação à posição inicial >>>
obj1.pos Vec(300, 50)

Para iniciar a simulação, basta chamar o comando

>>> world.run()                                            # doctest:
+SKIP

anterior. Veja como fica a posição final do objeto.

>>> obj1.imove(150, 0)  # deslocamento com relação à posição inicial >>>
obj1.pos Vec(300, 50)

Para iniciar a simulação, basta chamar o comando

>>> world.run()                                            # doctest:
+SKIP

anterior. Veja como fica a posição final do objeto.

>>> obj1.imove(150, 0)  # deslocamento com relação à posição inicial >>>
obj1.pos Vec(300, 50)

Para iniciar a simulação, basta chamar o comando

>>> world.run()                                            # doctest:
+SKIP

Objetos dinâmicos

Apesar do FGAme não fazer uma distinção explícita durante a criação, os objetos no mundo podem ser do tipo dinâmicos, cinemáticos ou estáticos. Todos eles participam das colisões normalmente, mas a resposta física pode ser diferente em cada caso. Os objetos dinâmicos se movimentam na tela e respondem às forças externas e de colisão. Os objetos cinemáticos se movimentam (usualmente em movimento retilíneo uniforme), mas não sofrem a ação de forças. Já os objetos estáticos simplesmente permanecem parados.

A diferenciação é feita apenas pelo valor das massas e das velocidades. Convertemos um objeto em cinemático atribuindo um valor infinito para a massa. Um objeto será estático se a massa for infinita e a velocidade nula.

>>> obj2.mass = 'inf' # automaticamente se torna estático pois a
velocidade ...                   # é nula

O FGAme utiliza esta informação para acelerar os cálculos de detecção de colisão e resolução de forças. As propriedades dinâmicas/estáticas dos objetos, no entanto são inteiramente transparentes ao usuário.

Vale observar que a condição de dinâmico vs. estático pode ser atribuída independentemente para as variáveis lineares e angulares. No segundo caso, o controle é feito pelo valor do momento de inércia no atributo .inertia do objeto. Para transformar um objeto dinâmico em inteiramente estático, seria necessário fazer as operações

>>> obj2.mass = 'inf' >>> obj2.inertia = 'inf' >>> obj2.vel
*= 0 >>> obj2.omega *= 0

De modo mais simples, podemos fazer todas as operações de uma vez utilizando os métodos .make_static() (ou kinematic/dynamic).

>>> obj2.make_static()

Já os métodos .is_static() (ou kinematic/dynamic) permitem investigar se um determinado objeto satisfaz a alugma destas propriedades.

>>> obj2.is_dynamic() False >>> obj2.is_static() True

Outra alternativa é simplesmente criar o objeto com um valor infinito para a massa

>>> obj3 = Circle(10, pos=(300, 300), mass='inf') >>> world.add(obj3)

Lembramos que as colisões são calculadas apenas se um dos objetos envolvidos for dinâmico. Deste modo, quando dois objetos cinemáticos ou um objeto estático e um cinemático se encontram, nenhuma força é aplicada e eles simplemente atravessam um pelo outro.

Aplicando forças

Forças externas

A FGAme se preocupa em calcular automaticamente as forças que surgem devido à colisão, atrito, vínculos, etc. Em alguns casos, no entanto, o usuário pode querer especificar uma força externa arbitrária que é aplicada a cada frame em um determinado objeto.

Isto pode ser feito salvando qualquer função do tempo no atributo especial Body.force() dos objetos físicos. Esta força será recalculada a cada frame em função do tempo (e implicitamente também pode depender de outras variáveis como posição, velocidade, etc).

>>> def gravity_force(t):
...     return Vec(0, -100) >>> obj3.force =
gravity_force

Agora o círculo obj3 é influenciado por uma força gravitacional. Existem várias forças já implementadas e vários métodos mais avançados de manipular o atributo .force que podem ser encontrados no módulo FGAme.physics.

Forças elementares

O método mostrado para definir forças externas na seção anterior é bastante poderoso, mas talvez seja um bocado inconveniente para definir forças globais como é o caso da gravidade. Normalmente queremos aplicar a gravidade à todos (ou quase todos) objetos do mundo simultaneamente e o método descrito anteriormente seria bastante inconveniente. A FGAme permite configurar as forças de gravidade e forças viscosas lineares e angulares de maneira global.

Na realidade, não definimos as forças diretamente, mas sim as acelerações que elas provocam em cada objeto. São as constantes gravity, damping e adamping. As forças são criadas a partir da fórmula:

F = obj.mass * (gravity - obj.vel * damping)

E o torque é gerado por:

tau = -obj.inertia * adamping *  obj.omega

Estas constantes podem ser definidas globalmente num objeto do tipo World ou individualmente caso um objeto queira ter um comportamento diferente do global.

>>> world.gravity = (0, -50)
>>> world.adamping = 0.1
>>> obj2.gravity =
(0, 50)  # objeto 2 cai para cima!

Todos objetos que não definirem explicitamente o valor destas constantes assumirão os valores definidos no mundo no qual estão inseridos.

Simulação simples

Uma simulação de física pode ser criada facilmente adicionando objetos a uma instância da classe World(). O jeito mais recomendado, no entanto, é criar uma subclasse pois isto melhora a organização do código e a sanidade do desenvolvedor. No exemplo abaixo, montamos um sistema “auto-gravitante” onde as duas massas estão presas entre si por molas

from FGAme import *

class GravityWorld(World):

    def init(self):
        # Criamos dois objetos
        A = Circle(20, pos=pos.from_middle(100, 0), vel=(100, 300),
                   color='red')
        B = Circle(20, pos=pos.from_middle(-100, 0), vel=(-100, -300))
        self.A, self.B = A, B
        self.add([A, B])

        # Definimos a força de interação entre ambos
        K = self.K = A.mass
        self.A.force = lambda t: -K * (A.pos - B.pos)
        self.B.force = lambda t: -K * (B.pos - A.pos)

        # Redefinimos a constante de amortecimento
        self.damping = 0.5

        # Definimos uma margem de 10px de espessura que os objetos não
        # conseguem atravessar
        self.add_bounds(width=10)

Agora que temos uma classe mundo definida, basta iniciá-la com o comando

if __name__ == '__main__':
    world = GravityWorld()
    world.run()

Interação com o usuário

Até agora vimos apenas como controlar os parâmetros de simulação física. É lógico que em um jogo deve ser existir alguma forma de interação com o usuário. Na FGAme, esta interação é controlada a partir da noção de eventos e callbacks. É possível registrar funções que são acionadas sempre que um determinado evento ocorre. Eventos podem ser disparados pelo usuário (ex.: apertar uma tecla), ou pela simulação (ex.: ocorrência de uma colisão).

Digamos que a simulação deva pausar ou despausar sempre que a tecla de espaço for apertada. Neste caso, devemos ligar o evento “apertou a tecla espaço” com a função .toggle_pause() do mundo, que alterna o estado de pausa da simulação.

>>> on_key_down('space', world.toggle_pause)               

A tabela abaixo mostra os eventos mais comuns e a assinatura das funções de callback

Evento Argumento Descrição
key-down tecla

Chamado no frame que uma tecla é pressionada. O argumento pode ser um objeto ‘tecla’, que depende do back end utilizado ou uma string, que é portável para todos back ends.

A string corresponde à tecla escolhida. Teclas especiais podem ser acessadas pelos seus nomes como em ‘space’, ‘up’, ‘down’, etc.

Os callbacks do tipo ‘key-down’ são funções que não recebem nenhum argumento.

key-up tecla Como em ‘key-down’, mas é executado no frame em que a tecla é liberada pelo usuário.
long-press tecla Semelhante aos anteriores, mas é executado em todos os frames em que a tecla se mantiver pressionada.
mouse-motion nenhum

Executado sempre que o ponteiro do mouse estiver presente na tela.

O callback é uma função que recebe um vetor com a posição do mouse como primeiro argumento.

mouse-button-down mouse-button-up mouse-long-press botão

Semelhante aos eventos de ‘key-down’, ‘key-up’ e ‘long-press’. Deve ser registrada com ‘left’, ‘right’, ‘middle’, ‘wheel-up’ ou ‘wheel-down’.

A grande diferença está em que o callback recebe a posição do ponteiro do mouse como primeiro argumento.

Um método prático de definir associar um método de uma classe a um evento especifico é utilizar o decorador @listen. Funciona de maneira semelhante às funções on_key_down(), on_key_up(), etc, mas exige um sinal como primeiro argumento.

class GravityWorld(World):
    ...

    @listen('key-down', 'space')
    def toggle(self):
        self.toggle_pause()

    @listen('long-press', 'right')
    def move_right(self):
        self.A.imove(5, 0)

    @listen('long-press', 'left')
    def move_left(self):
        self.A.imove(-5, 0)

    @listen('long-press', 'up')
    def move_up(self):
        self.A.imove(0, 5)

    @listen('long-press', 'down')
    def move_down(self):
        self.A.imove(0, -5)

Pronto! Agora você já sabe o básico para criar um jogo ou simulação simples utilizando a FGAme. Nas próximas seções vamos revisar com mais detalhes como a FGAme funciona e os princípios gerais de implementação e organização de um motor de jogos orientado à física.

Exemplo: interação com o mouse

Vamos modificar o exemplo anterior para que seja possível adicionar novos círculos utilizando o mouse. Queremos definir a posição inicial no instante em que o botão esquerdo do mouse é clicado e a velocidade seria dada pela posição relativa quando o usuário soltar o botão. Podemos dividir este procedimento em 3 etapas:

Frame em que o botão é pressionado:
Acrescenta o círculo e pausa a simulação
Enquanto o botão é pressionado:
Desenha uma linha na tela ligando o centro do círculo ao ponto atual.
Após o usuário soltar o botão:
Calcula a velocidade a partir da linha e remove-a do mundo. Restaura a simulação.

Podemos implementar cada uma destas etapas ouvindo os eventos mouse-button-down, mouse-long-press e mouse-button-up, respectivamente. O primeiro evento, que consiste em pausar a simulação e acrescentar o círculo pode ser implementado como:

class GravityWorld(World):
    ...

    @listen('mouse-button-down', 'left')
    def add_circle(self, pos):
        self.pause()
        self.circle = Circle(20, pos=pos, color='random')
        self.line = draw.Segment(pos, pos)
        self.add([self.circle, self.line])

Observe que a função add_circle() possui um argumento adicional pos que determina a posição do cursor do mouse na tela. Isto difere um pouco dos eventos key-up e key-down que não pedem argumentos adicionais.

Pausamos a simulação com o método FGAme.World.pause() e posteriormente criamos os atributos circle e line para armazenar o círculo recém criado e a linha que define o vetor de velocidade. Note que criamos o segmento de reta a partir da classe :cls:`FGAme.draw.Segment`. Todos os objetos definidos no módulo FGAme.draw definem uma interface de renderização mas não participam da física. Isto é útil para desenhar elementos gráficos do jogo sem se preocupar que eles possam sair por aí colidindo com os outros objetos na tela. O módulo FGAme.draw possui classes correspondentes à todos os objetos físicos definidos em FGAme, além de algumas outras classes adicionais.

Note que é necessário adicionar a linha e o círculo ao mundo com o método FGAme.World.add() para que sejam mostrados na tela e possam interagir com os outros objetos físicos.

Esta função implementa a lógica de pausar a simulação e acrescentar o círculo quando o clique inicia. Note que após soltar o mouse, a simulação permanece parada. Devemos ouvir o mouse-long-press para atualizar a linha e o mouse-button-up para continuar a simulação.

class GravityWorld(World):
    ...
    @listen('mouse-long-press', 'left')
        def set_circle_velocity(self, pos):
            self.line.end = pos


    @listen('mouse-button-up', 'left')
        def launch_circle(self, pos):
            self.resume()
            self.remove(self.line)
            self.circle.vel = 4 * self.line.direction

O handler de mouse-long-press simplesmente atualiza a posição do ponto final da linha na tela. Quando o usuário larga o botão, executamos o evento mouse-button-up, que despausa a simulação, remove a linha e define a velocidade do círculo como sendo proporcional ao vetor de direção do segmento de reta.