Implementando aceleração [parte 2 - estilingue]

8 minute read

Neste post vamos incrementar um pouco mais implementação de gravidade adicionando uma forma de definir a velocidade inicial de um corpo. Neste exemplo implementar um estilingue (de onde eu venho os maloqueiros chamam de badoque). Este programa é uma evolução do programa desenvolvido no post sobre gravidade.

Iniciando

Neste programa a bola vai poder viajar em qualquer direção, não apenas cair, uma vez que vamos ser capazes de dar uma velocidade inicial para a bola. A direção e a velocidade inicial do corpo serão definidas pelo ponto onde iniciamos o movimento.

O movimento inicia quando o usuário clica, mantém o clique e arrasta. Nesse momento o programa registra o ponto do clique e mostra ao usuário uma linha que liga o ponto atual do mouse e o ponto onde iniciou-se o movimento. Dê uma olhada abaixo para entender melhor o que vamos alcançar com este post.

Estilingue funcionando

Modificando a classe Bola

Agora que nossa bolinha vai poder se mover em qualquer direção precisamos adicionar a velocidade X também:

1
2
3
4
5
6
7
class Bola:
    def __init__(self, x, y, vel_x, vel_y):
        self.x = x
        self.y = y
        self.raio = 10
        self.velocidade_x = vel_x
        self.velocidade_y = vel_y

Coeficiente elástico

Você deve ter percebido também que dessa vez a bola não fica quicando indefinidamente, depois de um tempo ela para. No caso das colisões, introduzimos uma variável responsável por controlar quanto da velocidade será mantida após uma colisão. Para colisões que tentam simular o mundo real sempre temos uma perda de energia, então o valor para essa variável (coeficiente_restituicao) será menor que 1. Veja como ficou o método __init__:

1
2
3
4
5
6
7
8
def __init__(self):
    pyxel.init(200, 200)
    pyxel.mouse(True)
    self.bolas = []
    self.ponto_clique = None
    self.gravidade = 1
    self.coeficiente_restituicao = 0.9
    pyxel.run(self.update, self.draw)

Tratando a interação com o usuário

Precisamos agora tratar a interação com o usuário. Como mencionado anteriormente, o usuário clica na tela, mantém o botão esquerdo do mouse pressionado e arrasta o mouse. Quando o botão é liberado a bola é lançada na direção apontada pela linha exibida na tela. Para isso foi criada uma função chamada tratar_interacao_usuario.

1
2
3
4
5
6
7
8
9
10
def tratar_interacao_usuario(self):
    if pyxel.btnp(pyxel.MOUSE_LEFT_BUTTON):
        self.ponto_clique = (pyxel.mouse_x, pyxel.mouse_y)

    if pyxel.btnr(pyxel.MOUSE_LEFT_BUTTON):
        delta_x = self.ponto_clique[0] - pyxel.mouse_x
        delta_y = self.ponto_clique[1] - pyxel.mouse_y
        bola = Bola(pyxel.mouse_x, pyxel.mouse_y, delta_x//3, delta_y//3)
        self.bolas.append(bola)
        self.ponto_clique = None    

A linha 2 detecta quando o botão esquerdo do mouse é pressionado. Nesse momento essa posição é registrada na variável ponto_clique através de uma tupla. A linha 5 por sua vez identifica quando o botão é liberado. Nesse momento calculamos a distância, nos dois eixos, entre o ponto onde iniciou-se o movimento e o ponto onde o botão foi liberado. Esses valores são armazenados em delta_x e delta_y. É criada uma nova bola na posição atual do mouse e as velocidades para os eixos X e Y são passadas também para o construto. É feita uma divisão por 3 para que o valor não seja tão alto. Depois disso a bola é adicionada à lista de bolas (linha 9) e o ponto do clique é apagado (linha 10).

O método update

Vejamos agora como ficou o método update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def update(self):
    self.tratar_interacao_usuario()

    for bola in self.bolas:
        bola.x += bola.velocidade_x
        bola.velocidade_y += self.gravidade
        bola.y += bola.velocidade_y            

        if bola.x + bola.raio >= pyxel.width:
            bola.x = pyxel.width - bola.raio - 1
            bola.velocidade_x = int(-bola.velocidade_x * self.coeficiente_restituicao)

        if bola.x - bola.raio <= 0:
            bola.x = bola.raio + 1
            bola.velocidade_x = int(-bola.velocidade_x * self.coeficiente_restituicao)

        if bola.y + bola.raio >= pyxel.height:
            bola.y = pyxel.height - bola.raio - 1
            bola.velocidade_y = int(-bola.velocidade_y * self.coeficiente_restituicao)            

        if bola.y - bola.raio <= 0:
            bola.y = bola.raio + 1
            bola.velocidade_y = int(-bola.velocidade_y * self.coeficiente_restituicao)

        if abs(bola.y + bola.raio - pyxel.height) < 2:
            bola.velocidade_x *= 0.9

A primeira linha do método invoca o comportamento que acabamos de implementar para interação do usuário. O restante do método é responsável por tratar a colisão das bolas com as bordas laterais, superior e inferior da janela. Queremos dar a sensação de que a bola está colidindo com as bordas da janela.

As linhas 5-7 são bem parecidas com o exemplo da gravidade, estamos apenas aqui também atualizando a posição X de acordo com a velocidade X, o que não acontecia no exemplo anterior. Tomemos as linhas 9-11 como exemplo para o tratamento da colisão. A detecção da colisão é feita pelo cabeçalho do if: if bola.x + bola.raio >= pyxel.width:. Precisamos fazer a soma da posição X da bola com seu raio porque a coordenada controlada pelo sistema é do centro do círculo, e todos os pontos dele devem permanecer dentro da janela. Caso a expressão desse cabeçalho seja avaliada para True a bola está em uma posição não permitida, então precisamos faze-la retornar a uma posição válida (linha 10). Subtraímos 1 para jogar a bola 1 pixel para a esquerda, pois percebi que se isso não for feito a cada colisão o sistema perdia ainda mais energia do que foi especificado pelo coeficiente de restituição. Na linha 11 fazemos a inversão da velocidade e a multiplicamos pelo coeficiente de restituição, o que faz com que a bole perca 10% de sua velocidade a cada colisão.

As linhas 25-26 também são interessantes, elas adicionam um efeito de atrito entre as bolas e o piso. Se for detectado que a bola está encostando no piso, ela também perde 10% de sua velocidade no eixo X (movimento lateral).

O método draw

Este método draw traz as modificações necessárias para produzir o efeito desejado.

1
2
3
4
5
6
7
8
9
10
def draw(self):
    pyxel.cls(1)
    if self.ponto_clique is not None:
        pyxel.line(self.ponto_clique[0], self.ponto_clique[1], pyxel.mouse_x, pyxel.mouse_y, 7)
        pyxel.circ(pyxel.mouse_x, pyxel.mouse_y, 10, 10)
        pyxel.circb(pyxel.mouse_x, pyxel.mouse_y, 10, 9)

    for bola in self.bolas:
        pyxel.circ(bola.x, bola.y, bola.raio, 10)
        pyxel.circb(bola.x, bola.y, bola.raio, 9)

O if da linha 3 só terá seu corpo executado caso o usuário esteja manipulando uma bola, sinalizado pela variável ponto_clique diferente de None. Quando isso acontecer desenhamos uma linha entre o ponto do clique e a posição atual do mouse. Depois disso desenhamos um círculo amarelo e um contorno laranja na posição do mouse.

Linhas 8-10 pintam as outras bolas que já existem. Da mesma forma que anteriormente, desenham um círculo amarelo e um contorno laranja sobre a posição onde se encontra cada bola.

Resumo

Neste post exploramos uma implementação simples do efeito de um estilingue. Incrementamos um pouco o que foi implementado no post sobre gravidade.

Este é o resultado completo da implementação:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import pyxel

class Bola:
    def __init__(self, x, y, vel_x, vel_y):
        self.x = x
        self.y = y
        self.raio = 10
        self.velocidade_x = vel_x
        self.velocidade_y = vel_y

class Jogo:
    def __init__(self):
        pyxel.init(200, 200)
        pyxel.mouse(True)
        self.bolas = []
        self.ponto_clique = None
        self.gravidade = 1
        self.coeficiente_restituicao = 0.9
        pyxel.run(self.update, self.draw)     

    def tratar_interacao_usuario(self):
        if pyxel.btnp(pyxel.MOUSE_LEFT_BUTTON):
            self.ponto_clique = (pyxel.mouse_x, pyxel.mouse_y)

        if pyxel.btnr(pyxel.MOUSE_LEFT_BUTTON):
            delta_x = self.ponto_clique[0] - pyxel.mouse_x
            delta_y = self.ponto_clique[1] - pyxel.mouse_y
            bola = Bola(pyxel.mouse_x, pyxel.mouse_y, delta_x//3, delta_y//3)
            self.bolas.append(bola)
            self.ponto_clique = None   

    def update(self):
        self.tratar_interacao_usuario()

        for bola in self.bolas:
            bola.x += bola.velocidade_x
            bola.velocidade_y += self.gravidade
            bola.y += bola.velocidade_y            

            if bola.x + bola.raio >= pyxel.width:
                bola.x = pyxel.width - bola.raio - 1
                bola.velocidade_x = int(-bola.velocidade_x * self.coeficiente_restituicao)

            if bola.x - bola.raio <= 0:
                bola.x = bola.raio + 1
                bola.velocidade_x = int(-bola.velocidade_x * self.coeficiente_restituicao)

            if bola.y + bola.raio >= pyxel.height:
                bola.y = pyxel.height - bola.raio - 1
                bola.velocidade_y = int(-bola.velocidade_y * self.coeficiente_restituicao)            

            if bola.y - bola.raio <= 0:
                bola.y = bola.raio + 1
                bola.velocidade_y = int(-bola.velocidade_y * self.coeficiente_restituicao)

            if abs(bola.y + bola.raio - pyxel.height) < 2:
                bola.velocidade_x *= 0.9


    def draw(self):
        pyxel.cls(1)
        if self.ponto_clique is not None:
            pyxel.line(self.ponto_clique[0], self.ponto_clique[1], pyxel.mouse_x, pyxel.mouse_y, 7)
            pyxel.circ(pyxel.mouse_x, pyxel.mouse_y, 10, 10)
            pyxel.circb(pyxel.mouse_x, pyxel.mouse_y, 10, 9)

        for bola in self.bolas:
            pyxel.circ(bola.x, bola.y, bola.raio, 10)
            pyxel.circb(bola.x, bola.y, bola.raio, 9)

Jogo()

Leave a comment