Skip to main content

13 posts tagged with "godot"

View All Tags

Thiago Lages de Alencar

1 - Performance

Mude o modo de renderização para Compatibility, não é como se você fosse precisar de qualquer renderização além do básico para desenhar uma janela na tela.

Compatibility

Ative Application > Run > Low Processor Mode para que tenha um delay entre as renderizações da janela e apenas renderizar se alguma mudança for detectada. Em jogos a tela muda constantemente, então esse tipo de delay e validação só atrapalham mas como estamos falando de GUI que altera bem menos, isto ajuda muito.

Low Processor Mode

2 - Window Title Bar

Por padrão o nosso sistema operacional nos providência o gráfico básico de uma janela e nos deixa responsável por desenhar o conteúdo dentro dela.

Not borderless

O lado positivo é que isto nos providência o básico de uma janela, como aqueles 3 botões no topo da direita (minimizar, máximizar, fechar, ...).

Porém note que, dependendo do sistema operacional, mais opções podem estar disponíveis! Se eu clicar com o botão direito na title bar do topo, podemos ver mais ações:

Not borderless again

Se ativarmos Display > Window > Size > Borderless, o sistema operacional deixará de adicionar a title bar no topo:

Borderless

Basicamente ele está assumindo que você mesmo irá desenhar a title bar no topo caso queira (normalmente em jogos isto não faz sentido).

info

Borderless ou não, ainda se trata de uma janela no seu sistema operacional então alguns atalhos podem continuar funcionando (Super + Up/Down/Left/Right, Alt + Space).

3 - Multiple Windows

Janelas abertas são tratadas como processos filhos, ou seja, o encerramento de uma janela pai irá encerrar os filhos.

Caso queiramos ter múltiplas janelas idênticas, igual a editores de textos e navegadores, precisamos ter certeza que a janela principal (processo inicial) não possa ser encerrado da maneira padrão (clickando no botão de fechar).

Podemos resolver isto escondendo a janela principal e apenas exibindo as subwindows.

Ative Display > Window > Size > Transparent para que o fundo cinza padrão não seja renderizado durante a execução.

Transparent background

note

Acredito que a cor padrão do canvas é preto, por isto deixar de pintar vai deixar a janela preta

Ative Display > Window > Per Pixel Transparency > Allowed para que o fundo realmente seja transparente (caso contrário vai ficar o canvas preto).

Per Pixel Transparency

warning

Existe uma configuração que eu ainda não entendi a necessidade: Rendering > Viewport > Transparent Background.

Mas a documentação menciona ela como necessária.

Como visto na sessão anterior...

Ative Display > Window > Size > Borderless para o sistema operacional deixará de adicionar a title bar no topo.

Invisible

Embora ela esteja transparente, ela ainda é uma janela como as outras. Podemos conferir que ela ainda aparece quando apertando Alt+Tab (ou apenas apertando Alt no Ubuntu).

Window invisible but exist

Agora nós temos que tratar inputs!

Primeiro podemos notar que se está janela for posta na frente de outra, ela não irá deixar de consumir os seus clicks (mesmo que você queira selecionar algo na janela de trás).

Para resolver isto podemos alterar janela raiz (criada quando seu programa inicia) para repassar adiante clicks do mouse.

func _ready() -> void:
get_window().mouse_passthrough = true

Segundo podemos notar que ela ainda está processando teclas (pode ser selecionada pelo Alt + Tab, fechada por Alt + F4, maximizada com Super + Up, etc).

Ative Display > Window > Size > No Focus para que ela não possa ser focada (até por atalhos).

Process invisible

Lembre que fechar o processo pai fecha todos os filhos, porém fechar todos os filhos não fecha o pai.

Isto quer dizer que o processo pai continua rodando mesmo se o usuário fechar todas as janelas filhas. Agora o usuário apenas conseguiria encerrar o programa pelo "gerenciador de tarefas" ou terminal.

Para tratar isto podemos ligar um signal a um método responsável por notar quando a quantidade de filhos mudar e encerrar o programa se necessário.

Signal child_order_changed

func _on_child_order_changed() -> void:
if get_child_count() == 0:
get_tree().quit()
note

Isto é apenas uma maneira de tratar!

Nós poderiamos checar a cada frame se todas as janelas foram fechadas, poderiamos fazer os filhos avisarem o pai quando fossem encerrados, etc.

Agora precisamos entender que cada janela aberta é uma subwindow. Existem dois tipos de subwindows:

  1. Subwindows
    • Quando sua janela pede ao sistema operacional para criar uma janela filha dela
    • Sua janela filha vai possuir a title bar padrão de janelas
  2. Embed subwindows
    • Quando sua janela simula outra janela dentro dela mesmo
    • Isto impossibilita ela de ser mover para fora da janela pai

Se estamos tratando de uma aplicação que possui múltiplas janelas, precisamos que ela se mova para fora da janela pai. Caso contrário isso ocorreria:

Subwindow + Half subwindow

A janela 2 está saindo do limite da janela pai.

Poderiamos inicializar a janela pai maximizada para evitar isto porém outros problemas iriam aparecer, por exemplo: Janela pai ignorar clicks e teclas, tornando impossível interagir com as janelas simuladas nele.

Desative Display > Window > Subwindows > Embed Subwindows para que as subwindows sejam tratadas como janelas reais pelo sistema operacional (em vez de simuladas pela janela pai).

Subwindows

Mas se quisermos ter uma title bar de janela única para as nossas janelas? Podemos fazer o mesmo que fizemos com janela principal, torna-la borderless.

subwindows borderless

Dentro das propriedades da Janela, ative Flags > Borderless.

subwindows borderless activate

Agora nós seriamos responsáveis por criar a title bar no topo da janela. Desta maneira poderiamos fazer uma title bar única igual ao Google Chrome ou Steam!

4 - Custom Title Bar

Ter uma title bar própria é relativamente raro hoje em dia, pois muitas vezes requer reinventar a roda sem trazer benifícios reais.

Mas isto não quer dizer que nenhuma aplicação faz isto:
Custom Title Bars
(Steam, GNOME Files, Google Chrome)

Note que as 3 aplicações aproveitaram o espaço para providênciar mais informações e funcionalidades ao usuário. Porém nós vamos focar em pelo menos reproduzir o básico:

  1. Exibir titulo
  2. Providênciar botões de minimizar, maximizar e fechar
  3. Double click maximizar
  4. Arrastar a title bar deve mover a janela
  5. Redimensionar janela se arrastar as bordas

Depois disso você deve ser capaz de adicionar ou remover mais utilidades conforme a sua vontade.

warning

Estarei partindo do princípio que queremos customizar uma title bar na janela principal, por isto o código utiliza get_window(), mas adaptações podem ser necessárias caso esteja tratando subwindows.

Exibir Titulo

Basta utilizar o node Label.

Minimize, Maximize, Close Buttons

Basta utilizar 3 nodes Button tratando o signal pressed:

func _on_minimize_pressed() -> void:
get_window().mode = Window.MODE_MINIMIZED


func _on_maximize_pressed() -> void:
if get_window().mode == Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
else:
get_window().mode = Window.MODE_MAXIMIZED


func _on_close_pressed() -> void:
get_tree().quit()
subwindows tip

É importante tratar o signal close_requested vindo da janela, pois é por ele que você recebe notificações que o usuário tentou fechar de outras maneiras (taskbar do windows, etc).

Double Click Maximize

Container não possui signal para isto diretamente porém podemos utilizar o signal mais geral gui_input.

func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()


func _on_title_bar_double_click() -> void:
match get_window().mode:
Window.MODE_MAXIMIZED:
get_window().mode = Window.MODE_WINDOWED
_:
get_window().mode = Window.MODE_MAXIMIZED

Já estamos dividindo em funções menores pois os passos seguintes irão adicionar mais funcionalidades nestas funções gerais.

Drag Window

A princípio, arrastar a janela pode ser resumido em saber duas coisas:

  • Saber se o click do mouse está sendo pressionado
  • Onde que o click estava quando começou
var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_title_bar_mouse_button(event)
elif event is InputEventMouseMotion:
_on_title_bar_mouse_motion(event)


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
_on_title_bar_double_click()
elif event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_title_bar_dragging = true
_title_bar_dragging_start = get_global_mouse_position()
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_title_bar_dragging = false


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
if _title_bar_dragging:
_on_title_bar_dragged()


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
subwindows tip

Primeiro: Talvez seja bom mover para o centro da tela a janela pois a posição poder não estar correta durante a inicialização (bug?):

func _ready() -> void:
get_window().move_to_center()

Segundo: Talvez seja necessário utilizar get_local_mouse_position() em vez de get_global_mouse_position() pois deve ser necessário o canvas da própria subwindow.

Esse foi apenas o essencial sobre arrastar, agora podemos pensar em implementar detalhes sobre a ação de arrastar janelas.

Por exemplo: Quando o usuário tentar arrastar uma janela máximizada, ela automaticamente sai do máximizado e se posiciona para que o mouse esteja proporcionalmente na posição correta.

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
match get_window().mode:
Window.MODE_WINDOWED:
get_window().position += get_global_mouse_position() as Vector2i - _title_bar_dragging_start
Window.MODE_MAXIMIZED:
_title_bar_dragging_adjustment = get_global_mouse_position().x / get_window().size.x
get_window().mode = Window.MODE_WINDOWED


func _on_resized() -> void:
if _title_bar_dragging_adjustment != 0:
get_window().position += (get_global_mouse_position() as Vector2i)
get_window().position.x -= get_window().size.x * _title_bar_dragging_adjustment
_title_bar_dragging_start = get_global_mouse_position()
_title_bar_dragging_adjustment = 0

Resize Window

Redimensionar pode ser facilmente implementado se utilizarmos o node MarginContainer que nos permite adicionar bordas às laterais, estas serão nossas bordas que devem reagir ao mouse.

Nodes do tipo Control possuem lógica para lidar com inputs do mouse, eles podem consumir ou passar ao node de cima as input do mouse.

Isso quer dizer que qualquer input do mouse na nossa janela (que não tiver sido consumida) chegará ao nossoMarginContainer. Isto não é o que queremos, para nós só é interessante que chegue inputs interagindo com a borda do nosso container.

Podemos resolver isto parando o consumo de inputs no container logo abaixo do MarginContainer:

Margin

Agora temos certeza que interações vindo do signal gui_input são interações diretas com o MarginContainer.

enum Margin {
NONE,
TOP,
RIGHT,
BOTTOM,
LEFT,
TOP_RIGHT,
TOP_LEFT,
BOTTOM_RIGHT,
BOTTOM_LEFT,
}

var _margin_dragging: bool = false

var _margin_dragging_edge_start: Vector2i

var _margin_selected: Margin

var _title_bar_dragging: bool = false

var _title_bar_dragging_start: Vector2i

var _title_bar_dragging_adjustment: float = 0

func _get_current_margin() -> Margin:
var margin: Margin = Margin.NONE

if get_global_mouse_position().x < get_theme_constant("margin_left"):
margin = Margin.LEFT
elif get_global_mouse_position().x > size.x - get_theme_constant("margin_right"):
margin = Margin.RIGHT

if get_global_mouse_position().y < get_theme_constant("margin_top"):
match margin:
Margin.LEFT:
return Margin.TOP_LEFT
Margin.NONE:
return Margin.TOP
Margin.RIGHT:
return Margin.TOP_RIGHT
elif get_global_mouse_position().y > size.y - get_theme_constant("margin_bottom"):
match margin:
Margin.LEFT:
return Margin.BOTTOM_LEFT
Margin.NONE:
return Margin.BOTTOM
Margin.RIGHT:
return Margin.BOTTOM_RIGHT

return margin


func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
_on_mouse_button(event)
elif event is InputEventMouseMotion:
_on_mouse_motion(event)


func _on_mouse_button(event: InputEventMouseButton) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
_margin_dragging = true
_margin_selected = _get_current_margin()
_margin_dragging_edge_start = get_window().position + get_window().size
elif event.button_index == MOUSE_BUTTON_LEFT and not event.pressed:
_margin_dragging = false


func _on_mouse_motion(_event: InputEventMouseMotion) -> void:
if _margin_dragging:
_on_dragged()
else:
_on_hover()


func _on_dragged() -> void:
if get_window().mode != Window.MODE_WINDOWED:
return

var mouse_position: Vector2i = get_global_mouse_position()

match _margin_selected:
Margin.TOP:
get_window().position.y += mouse_position.y # TODO: Fixing moving window
get_window().size.y = _margin_dragging_edge_start.y - get_window().position.y
Margin.RIGHT:
get_window().size.x = mouse_position.x
Margin.BOTTOM:
get_window().size.y = mouse_position.y
Margin.LEFT:
get_window().position.x += mouse_position.x # TODO: Fixing moving window
get_window().size.x = _margin_dragging_edge_start.x - get_window().position.x
Margin.TOP_RIGHT:
get_window().position.y += mouse_position.y # Top
get_window().size = Vector2i(
mouse_position.x, # Right
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.TOP_LEFT:
get_window().position = Vector2i(
get_window().position.x + mouse_position.x, # Left,
get_window().position.y + mouse_position.y, # Top
)

get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
_margin_dragging_edge_start.y - get_window().position.y, # Top
)
Margin.BOTTOM_RIGHT:
get_window().size = Vector2i(
mouse_position.x, # Right
mouse_position.y, # Bottom
)
Margin.BOTTOM_LEFT:
get_window().position.x += mouse_position.x # Left
get_window().size = Vector2i(
_margin_dragging_edge_start.x - get_window().position.x, # Left
mouse_position.y, # Bottom
)


func _on_hover() -> void:
match _get_current_margin():
Margin.TOP:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.RIGHT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.BOTTOM:
mouse_default_cursor_shape = Control.CURSOR_VSIZE
Margin.LEFT:
mouse_default_cursor_shape = Control.CURSOR_HSIZE
Margin.TOP_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE
Margin.TOP_LEFT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_RIGHT:
mouse_default_cursor_shape = Control.CURSOR_FDIAGSIZE
Margin.BOTTOM_LEFT:
mouse_default_cursor_shape = Control.CURSOR_BDIAGSIZE


func _on_minimize_pressed() -> void:
...


func _on_maximize_pressed() -> void:
...


func _on_close_pressed() -> void:
...


func _on_title_bar_gui_input(event: InputEvent) -> void:
...


func _on_title_bar_mouse_button(event: InputEventMouseButton) -> void:
...


func _on_title_bar_double_click() -> void:
...


func _on_title_bar_mouse_motion(_event: InputEventMouseMotion) -> void:
...


func _on_title_bar_dragged() -> void:
...


func _on_resized() -> void:
...

Dentro das funções novas, muitas possuem a mesma lógica utilizada para arrastar janela. Porém duas possuem lógica nova: _get_current_margin() e _on_dragged()

A primeira é responsável por identificar a borda a qual o mouse se encontra (varias validações para identificar a posição do mouse em relação as bordas).

A segunda é a lógica de redimensionar, para resolver ela é recomendado primeiro resolver a lógica para cima, direita, baixo e esquerda (as diagonais são combinações das lógicas das outras).

note

Redimensionar uma janela inclui redimensionar os items dentro dela, isso pode ser um tanto quanto custoso de se fazer todas as frames.

Eu penso em testar redimensionar de tempos em tempos e apenas se tiver algum redimensionamento pendente 🤔.

references

Thiago Lages de Alencar
note

Pesquisa limitada pelo fato de Twitter ainda estar bloqueado no Brasil.

Aproveitando que eu estava olhando o drama em cima de Godot e assistindo Ace Attorney... Resolvi brincar com o site https://objection.lol.

Video

Details

Twitter

Discord

Godot Foundation

Website

jun-17

jul-08

oct-11

Fork

SMS

Docs

Twitter++

BlueSky

Google

Discord++

Youtube

Thiago Lages de Alencar

Forward And Backward Reaching Inverse Kinematics (FABRIK) é a última kinematic que veremos. É utilizada para cadeias de ossos também mas apenas precisamos de uma iteração para definir o estado final (diferentemente da CCD).

Corrente de 4 ossos

Forward And Backward Reaching

A idéia é duas caminhadas na cadeia de ossos, a primeira irá mover os ossos em direção ao alvo (forward) e a segunda vai voltar o osso a base (backward).

A operação importante a se entender durante as duas etapas é ação de alcançar um outro ponto (reaching). Vamos utilizar ela durante as duas etapas então é bom entender isto primeiro.

Reaching

Alcançar um alvo é dividido em 2 etapas, olhar para o alvo e mover até o alvo.

Um osso e um alvo fora do alcance

Na primeira etapa podemos utilizar a mesma lógica do look at ou usar a função que sua game engine disponibilizar para rotacionar até um ponto.

Osso rotacionando até o alvo

Para mover até o ponto é preciso usar o tamanho do osso e calcular onde seria o novo ponto da base do osso. Visualizar onde seria é algo bem simples:

Osso transparente onde o osso precisa estar no final

Calcular isso envolve conseguir criar um vetor que represente o osso. Primeiro precisamos saber o vetor em que o osso se encontra, o vetor do ponto inicial dele até o alvo.

Vetor = Posição do alvo - Posição do osso

Vetor do osso até o alvo

Com isto podemos calcular a proporção desse vetor com o vetor do osso. Em outras palavras, qual o tamanho do vetor do osso em relação a esse vetor? É duas vezes o tamanho deste? É três vezes o tamanho? É metade desse vetor?

Tamanho do vetor = √(Vetor.x² + Vetor.y²)
Proporção = Tamanho do osso / Tamanho do vetor

Utilizando essa proporção podemos criar um vetor do tamanho do osso.

Vetor osso = Vetor * Proporção

Vetor osso

A última coisa é calcular o novo ponto onde o osso deve inciar. Basta pegar o ponto do alvo e reduzir pelo vetor do osso.

Posição do osso = Posição do alvo - Vetor osso

Vetor osso começando no ponto onde o osso tem que terminar

Pronto, sabemos onde botar o osso e podemos mover ele para lá (caso você já não tenha movido ele na última operação)

Osso na posição correta

Forward

A primeira caminhada pela cadeia de ossos envolve fazer cada osso andar em direção (forward) ao osso seguinte. No caso da ponta da cadeia, ela irá mover em direção ao alvo.

TODO

TODO

O ponto vermelho irá representar onde globalmente o início do osso atual está, nós usamos ele para decidir onde o osso seguinte vai alcançar.

TODO

TODO

TODO

Estou pausando aqui para lembrar que movimentar e rotacionar um osso afeta todos os filhos, por isto os ossos filhos são movimentados e rotacionados de forma a ficarem "piores" (mais longe do alvo).

TODO

O osso seguinte irá utilizar o osso anterior como referência, seguimos essa tática para cada um dos ossos.

TODO

TODO

TODO

TODO

TODO

TODO

O ponto azul representa a base da cadeia e é um ponto de referência que usaremos na próxima caminhada.

Backward

note

A última caminhada deixou tudo uma bagunça mas isso apenas porque eu escolhi trata-los da mesma forma que minha game engine (Godot) trata nodes nela.

Se tivessemos usado um array de ossos em vez de relações pai e filho, um não afetaria o outro!

Nós focaremos agora a mover os ossos em direção a base, ou seja, eles caminharam para tráz (backward). Dessa vez não precisamos nos preocupar em rotacionar, apenas mover para o final do osso anterior.

TODO

TODO

TODO

TODO

TODO

TODO

TODO

TODO

Iteration

Ao final de uma iteração podemos ter algo errado como visto acima, mas se repetirmos mais vezes vamos começar a caminhar para algo melhor.

O que acontece se começarmos outra iteração? Vamos começar novamente rotacionando o osso da ponta.

TODO

Se você der zoom na imagem, vai notar que o último osso passou do ponto alvo. Mas a etapa seguinte é mover de forma que o ponto final do osso bata com a posição do alvo.

TODO

Bem, agora o osso seguinte está incorreto... Mas se continuarmos repetindo o processo...

TODO

Aos poucos os ossos vão indo para uma posição melhor, mas eu não pretendo mostra-lo uma segunda iteração pois eu fiz estes desenhos a mão. 🤣

Conclusion

O código simplificado em GDScript (linguagem do Godot):

for i in iterations:
_apply_forwards()
_apply_backwards(base_global_position)


func _apply_forwards() -> void:
# O osso da ponta vai morar no alvo, os seguintes vão tratar o anterior como alvo.
var target_global_position: Vector2 = target.global_position

# Esse array leva da ponta até a base.
for bone in chain:
# Rotaciona em direção ao alvo.
bone.look_at(target_global_position)

# Evita calcular ratio como infinito.
if target_global_position == bone.global_position:
continue

# Calcula a nova posição do osso.
var stretch: Vector2 = target_global_position - bone.global_position
var ratio: float = bone.get_bone_length() / stretch.length()
bone.global_position = target_global_position - stretch * ratio

# Define o alvo do osso seguinte.
target_global_position = bone.global_position


func _apply_backwards(base_global_position: Vector2) -> void:
# Esse array leva da ponta até a base, então agora precisamos caminhar ao contrário.
for i in range(chain.size() - 1, -1, -1):
var bone: Bone = chain[i]

bone.global_position = base_global_position

# Calcula a posição do osso seguinte.
var direction := Vector2(cos(bone.global_rotation), sin(bone.global_rotation))
base_global_position = bone.global_position + direction * bone.get_bone_length()

References

Thiago Lages de Alencar

Cyclic Coordinate Descent Inverse Kinematic (CCDIK) é diferente das lógicas anteriores, pois nós não sabemos qual o estado final que desejamos para os ossos. A ideia é fazer diversas iterações até que chegue em um resultado aceitável.

Em outras palavras, CCDIK se trata da jornada e não do resultado final.

Corrente de 4 ossos

Cyclic Coordinate Descent

Para cada osso temos que calcular a rotação para a ponta chegue mais perto do ponto desejado.

Dependendo da direção que você caminhar pela cadeia de ossos o movimento pode ser diferente. Nesse exemplo iremos fazer de tráz para frente (osso mais perto da ponta até osso mais longe da ponta).

Rotacionando osso 4

Rotacionnado osso 3

Rotacionnado osso 2

Rotacionnado osso 1

Após primeira iteração

E com isto fizemos a primeira iteração. Vamos começar a segunda iteração.

Rotacionando osso 4

Rotacionnado osso 3

Rotacionnado osso 2

Rotacionnado osso 1

Após segunda iteração

Cada iteração se aproximando mais do ponto desejado.

Após N iterações

O único cálculo que precisamos fazer toda iteração é o ângulo da ponta até o alvo.

Negative Scale

Recomendo ler no Two Bone sobre escala negativa. O que importa é que o mesmo se aplica neste caso, se uma das escalas for negativa, precisamos rotacionar na direção oposta.

Conclusion

Em GDScript o código seria algo como:

for bone in chain:
var angle_to_target: float = bone.global_position.angle_to_point(target.global_position)
var angle_to_tip: float = bone.global_position.angle_to_point(tip.global_position)
var angle_diff: float = angle_to_target - angle_to_tip

# Escala negativa ou não.
if bone.global_scale.sign().x == bone.global_scale.sign().y:
bone.rotate(angle_diff)
else:
bone.rotate(-angle_diff)
note

Normalmente toda iteração você verificaria se chegou em um resultado aceitável.

No meu caso (game engine), estou fazendo uma iteração por frame e sem pensar se chegou ou não em um resultado aceitável.

References

Thiago Lages de Alencar

Faz um mês desde que escrevi sobre inverse kinematic look at. Talvez eu esteja enrolando para falar desta pois foi por ela que eu comecei a ver inverse kinematics... e sofri muito.

Two bone inverse kinematic! Dado que queremos a mão em uma devida posição, como os dois ossos responsáveis pelo braço devem se encontrar?

Note que não vamos ditar onde a mão vai estar, porém onde desejamos que ela estivesse. Isso é importante pois o calculo muda dependendo se a mão alcança ou não a posição desejada.

Um braço dobrado e com a mão aberta

Two Bone

Braço estendido

O que você faz quando tenta alcançar algo longe de você?
Estica o máximo possível.

O que você faz quando tenta alcançar algo perto de você?
Curva o braço de forma que sua mão acabe na posição desejada.

Primeira coisa a se fazer é descobrir se está fora ou dentro do alcance 🤣.
Em outras palavras, a base do braço até o ponto desejado é maior ou menor que o braço todo?

Braço estendido com vetor para um ponto fora do alcance

Podemos descobrir a distância entre dois pontos se calcularmos o vetor entre eles e depois usarmos a clássica formúla para distância. Resumidamente:

  • P2-P1
  • √(x²+y²)

Sabendo disso podemos calcular as seguintes distâncias:

  • A -> T
    • Distância até posição desejada
  • A -> B
    • Tamanho do osso 1
  • B -> C
    • Tamanho do osso 2

Agora podemos verificar justamente se está dentro ou fora do alcance!

Distância até posição desejada > (Tamanho do osso 1 + Tamanho do osso 2)

Out of Range

Acontece que estender o braço em uma direção é apenas tornar o ângulo global dos ossos equivalentes ao da direção.

Mostrando o ângulo global do vetor

Mostrando o ângulo global do braço quando está na mesma direção do vetor

Já vimos em IK Look at como fazer um osso/vetor apontar para uma direção e isso é tudo que precisamos fazer aqui também.

  • Apontar osso 1 para posição desejada
  • Apontar osso 2 para posição desejada

Fim.

In range - Triangle

Espero que este desenho já deixe claro como utilizaremos trigonometria com braços curvados.

Mostrando que braços curvados podem ser vistos como triângulos

Neste caso o ponto onde desejamos posicionar a mão está dentro do alcance dela, então irá acabar sendo exatamente a posição da mão (utilizaremos C mas poderia ser T).

Mostrando um braço curvado e que utilizaremos as letras A,B,C para representar pontos e a,b,c para representar tamanho do lado do triângulo

Já calculamos os lados do triângulo, então agora vamos focar no seus ângulos internos (utilizaremos α β γ).

Mostrando um braço curvado e que utilizaremos as letras A,B,C para representar pontos e a,b,c para representar tamanho do lado do triângulo

Sabendo todos os lados do triângulo podemos utilizar leis do cossenos para descobrir cada ângulo interno:

a² = b² + c² - 2bc*cos(α)
b² = a² + c² - 2ac*cos(β)
c² = a² + b² - 2ab*cos(γ)

Sabendo os lados e sabendo os ângulos internos nós conseguimos dizer como o braço precisa estar dobrado. O problema é que ele ainda pode estar dessa forma de diversas maneiras 🤣:

Mostrando diferentes maneiras que o braço pode estar rotacionado

In range - Two Angles

Existem dois ângulos que estamos buscando descobrir, rotacionando eles conseguiremos os ossos exatamente onde queremos:

Mostrando rotação por rotação a se fazer em um braço que está inicialmente apontando para o eixo X

Nessa imagem o braço estava esticado em direção ao eixo X, rotacionamos osso 1 por θ1 e osso 2 por θ2 para obter o braço no formato que queriamos.

note

Eu sei que os desenhos tem ficado cada vez piores, eu deveria estar usando uma ferramenta apropriada ou organizando melhor os desenhos...

Mas a preguiça ganhou 🙂

Como podemos obter θ1?

Se você estava pensando "é só calcular o ângulo do eixo X até o osso 2 que você consegue o θ1", deixe-me lembra-lo que o braço vai começar de forma desconhecida.

Mesmo se estivesse esticado no eixo X, o osso 2 não vai estar na posição desejada ainda!

Mostrando o braço no eixo X e o ponto desejado acima dele

Mas sabe o que podemos fazer? Calcular o ângulo do eixo X até o ponto desejado (T).

Mostrando o ângulo do eixo X até o vetor feito do osso 1 até o ponto desejado

Sabe o porque eu chamei ele de α'? Porque ele está relacionado com α!

Acontece que para obter o ângulo desejado, podemos rotacionar até a direção de T e depois remover a rotação interna do triângulo (α).

Mostrando os ângulos α&#39; e α

Não precisamos literalmente rotacionar, podemos calcular o ângulo e depois rotacionar: α' - α

Mostrando que se reduzirmos α&#39; pelo ângulo interno α conseguimos o osso 1 apontando na direção certa

Como podemos obter θ2?

Felizmente o osso 2 não rotacionado faz um ângulo de 180º com o osso 1.

Mostrando que o osso 2 quando tem rotação 0º, faz um ângulo de 180º com osso 1

Se rotacionarmos por 180º e diminuirmos pelo ângulo interno (β), obtemos justamente o ângulo que queriamos.

Mostrando o ângulo de 180º e β para melhor ver que é possível conseguir o ângulo do osso 2

Novamente não precisamos literalmente rotacionar, podemos calcular o ângulo e depois rotacionar: 180º - β

Mostrando que se reduzirmos β do 180º conseguimos o osso 2 apontando corretamente

No final chegamos aos ângulos graças aos ângulos internos do triângulo:

θ1 = α' - α
θ2 = 180º - β

In range - Bend Direction

Mas se nós quisermos que o braço fique curvado para o outro lado?

Acontece que mesmo curvando para o outro lado, os valores internos do triângulo não se alteram.

Mostrando que mudar a direção que o braço curva não afeta o triângulo interno

Então todo o calculo se mantém até a última etapa, onde precisamos mudar o sinal da rotação interna.

θ1 = α' + α
θ2 = 180º + β

In range - Negative Scale

Quando você escala qualquer um dos eixos por negativo, você também está dizendo que a direção para qual ele está rotacionando trocou:

Vetor (1,1) antes e após escalar X por -1

Se agora escalarmos o eixo Y negativamente, a rotação irá voltar a ser igual o início.
Cada vez que você escala um eixo negativamente, você troca a direção das rotações.

Como isso afeta nossos calculos?

Apenas o ângulo que utiliza o eixo X como referência é afetado (pois o eixo X nunca é escalado negativamente)

Mesma imagem anterior porém mostrando o segundo ângulo do ponto de vista do eixo X

Agora não queremos reduzir do ângulo α', mas sim acrescentar:

θ1 = α' + α

Mas se quisermos o osso curvado para a outra direção? É, então queremos novamente reduzir...

θ1 = α' - α

Err... basicamente estamos bricando de jogo do troca, dependendo da situação queremos rotacionar para diferentes direções.

Conclusion

Este é o meu código escrito em GDScript (linguagem do Godot):

var flip_bend: bool = false
var target_distance: float = bone_one.global_position.distance_to(target.global_position)
var bone_one_length: float = bone_one.get_bone_length()
var bone_two_length: float = bone_two.get_bone_length()
var angle_to_x_axis: float = (target.global_position - bone_one.global_position).angle()

# Fora do alcance.
if target_distance > bone_one_length + bone_two_length:
bone_one.global_rotation = angle_to_x_axis
return

# Lei dos cossenos.
var angle_0: float = acos(
(target_distance ** 2 + bone_one_length ** 2 - bone_two_length ** 2) / (2 * target_distance * bone_one_length)
)

var angle_1: float = acos(
(bone_two_length ** 2 + bone_one_length ** 2 - target_distance ** 2) / (2 * bone_two_length * bone_one_length)
)

# Direção da curva do braço.
if flip_bend:
angle_0 = -angle_0
angle_1 = -angle_1

# Escala negativa ou não.
if bone_one.global_scale.sign().x == bone_one.global_scale.sign().y:
bone_one.global_rotation = angle_to_x_axis - angle_0
else:
bone_one.global_rotation = angle_to_x_axis + angle_0

bone_two.rotation = PI + angle_1

Extra - Negative Scale in Godot

Este é extra pois depende muito da ferramenta que está utilizando, no meu caso Godot em 2D.

Godot representa translação, rotação e escala utilizando matriz. Entenda mais sobre transforms na documentação do Godot, aqui iremos direto ao assunto.

Matriz identidade representa um transform sem alteração nenhuma (translação, rotação e escala)

Matriz identidade

A desvantagem de utilizar uma matriz para armazenar todas essas informações é que algumas são impossíveis de extrarir corretamente. Olhe a matriz após escalar X por -1:

Matriz com X escalado por -1

Agora olhe a matriz após rotacionar por 180º e escalar Y por -1:

Mesma matriz apresentada anteriormente

Exatamente a mesma matriz... Se você der essa matriz para Godot, ele vai assumir que você fez a segunda opção (rotacionou e escalou Y por -1).

Como isso afeta nossa Inverse Kinematic?

Não afeta se você utilizou funções que já levam esse problema em conta, porém se vc operou diretamente sobre os transforms... Você talvez note alguns problemas.

References

Thiago Lages de Alencar

Dado que já refleti sobre forward kinematics, está na hora de falar sobre inverse kinematics (por mais que eu esteja com preguiça de fazer isso).

Vamos começar por algo que eu até me questiono se seria inverse kinematic: Look at.

Ser capaz de fazer uma mão apontar para uma posição ou a cabeça olhar para uma direção.

Jogador 2D movendo a cabeça para cima

Look At

Lembrando que articulações sempre possuem as suas informações locais e que as informações globais são facilmente calculáveis, nossa tarefa é descobrir como queremos alterar qualquer uma delas para alcançar nosso objetivo.

No caso, nosso objetivo é fazer com que o vetor da articulação aponte para X.

Uma imagem com o vector apontando para um X na direita dele e outra imagem igual porém com o X movido para cima do vetor

Na primeira parte da imagem, temos o vetor já apontando em direção do X.
Na segunda parte da imagem, o X se encontra a 90º graus do vetor.

Olhando a imagem nós conseguimos saber que para continuar apontando para X temos que rotacionar por 90º, mas como conseguir este ângulo matemáticamente?

Talvez você já tenha notado mas vamos fazer isto usando trigonometria (se prepare que IK é triângulo para tudo que é lado).

0º ~ 90º

Um bom começo é sabermos calcular o ângulo para uma posição, sem se preocupar com detalhes como global e local.

Vetor com ângulo desconhecido entre 0º e 90º

Funções trigonométricas são o segredo para trabalhar com triângulos (seno, cosseno e tangente) e neste caso tangente é justamente o que procuramos.

tan θ = cateto oposto / cateto adjacente

Agora nós temos o valor da tangente para qualquer posição (x,y).

  • (1,1)
    • tan(θ) = 1
  • (3,2)
    • tan(θ) = 2/3
  • (4,7)
    • tan(θ) = 7/4

Se você bem se lembra, existem funções trigonométricas inversas que são justamente quem vão nos dar o ângulo que chega ao valor que temos.

  • (1,1)
    • tan(θ) = 1
      • atan(1) = θ
        • 45
  • (3,2)
    • tan(θ) = 2/3
      • atan(2/3) = θ
        • 33.690067526
  • (4,7)
    • tan(θ) = 7/4
      • atan(7/4) = θ
        • 60.255118703

0º ~ 360º

Um problema que cedo ou tarde iriamos notar é que dos valores da tangete não é possível definir qual quadrante se trata.

Imagem mostrando que não da pra decompor o ângulo a partir do valor da tangente

atan(1) pode ser 45º ou 225º
atan(-1) pode ser 135º ou 315º

Única maneira de saber exatamente o quadrante é sabendo o sinal dos eixos.

  • (positivo,positivo)
    • atan(θ) + 0º
  • (negativo,positivo)
    • atan(θ) + 90º
  • (negativo,negativo)
    • atan(θ) + 180º
  • (positivo,negativo)
    • atan(θ) + 270º

É por isso que em muitas bibliotecas matemáticas existe a função atan(v) e a função atan2(x, y).
A segunda utiliza os eixos para saber o real ângulo.

Rotating

Agora que sabemos como obter o ângulo do ponto (0,0) até uma posição qualquer, podemos finalmente rotacionar a articulação.

O próximo problema é que não estamos falando de rotacionar a partir do ponto (0,0) mas sim da posição da articulação.

Mostrando diferença entre o ângulo local e global

θ1: Rotação global, usando ponto (0,0) como referência.
θ2: Rotação local, usando a posição da articulação como referência.

Para solucionar isto podemos calcular a posição do ponto em relação a articulação:

posição do ponto relativa à articulação =
posição global do ponto - posição global da articulação

Exemplo:

Posição global do ponto: (35, 10)
Posição global da articulação: (25, 10)
Posição do ponto relativa à articulação: (10, 0)
Ângulo: 0º

Se movermos o ponto:

Posição global do ponto: (50, 30)
Posição global da articulação: (25, 10)
Posição do ponto relativa à articulação: (25, 20)
Ângulo: 38º

Pronto, agora sabemos qual deveria ser a rotação daquela articulação!

Conclusion

Você provavelmente não terá que pensar em nada disso pois muitas game engines já possuem métodos para lidar com isto, por exemplo em Godot podemos encontrar algo como:

Node2D.get_angle_to(global_position)

Essa função retorna o ângulo global que falta para estar em direção ao ponto global.

References

Thiago Lages de Alencar

Esses últimos meses eu tenho gastado um grande tempo (mais do que gostaria) vendo o código de inverse kinematics 2D do Godot.

E eu preciso passar a limpo o conhecimento básico que eu possuo, para melhor garantir que não estou cagando tudo no código deles.

Mesma cara do meme &quot;awkward look monkey puppet&quot;

Robotic Arms

É difícil falar de forward kinematics (FK) e inverse kinematics (IK) sem entender braços robóticos, pois foi o primeiro uso destas lógicas.

Para entender melhor um braço, vamos dividi-lo em 4 partes:

  • Base
  • Limbs (membros)
  • Joints (articulações)
  • End effector (mão do braço)

Braço robótico

  • Base, providência estabilidade para o braço
  • Limbs, separa as partes entre si
  • Joints, são responsáveis por se rotacionar
  • End effector, interage com o objeto

A parte que mais damos atenção quando falamos de FK e IK são joints, pois elas providência a lógica de movimentação do braço.

FK & IK

Se tivessemos que resumir cada assunto, seria algo por parte de:

  • Forward kinematics
    • Foca em descobrir o estado da mão, dado que o braço está em certo estado.
  • Inverse kinematics
    • Foca em descobrir o estado do braço, dado que deseja a mão em certa estado.
note

Quando eu digo "estado", estou me referindo a posição e rotação dos respectivos componentes.

FK

FK

IK

IK

Forward Kinematic

Minha kinematic favorita por ser a mais simples de calcular, onde tudo se resume a um grande somatório.

Braço robótico totalmente vertical

Adicionei setas nessa imagem para represetarem as rotações locais das joints (articulações), essa rotação se refere ao quanto aquela joint (articulação) se rotacionou.

Braço robótico totalmente vertical com ângulos clássicos

Agora adicionei uma circuferência mostrando os ângulos clássicos e podemos usar eles como referência para a rotação global, essa rotação é sempre relacionada à direita (eixo X).

Para começar nosso experimento, podemos rotacionar o end effector (mão) por 90 graus e ver o que podemos concluir.

Mão rotacionada 90 graus

  • End effector
    • Rotação local: 90º
    • Rotação global: 90º
  • Joint 2
    • Rotação local: 0º
    • Rotação global: 0º
  • Joint 1
    • Rotação local: 0º
    • Rotação global: 0º

Podemos ver que rotacionar um ponto localmente não afeta os anteriores (os pais). Também importante notar que rotação local sempre irá afetar a rotação global daquele ponto.

Mas se tivessemos rotacionado a joint 2 por 90 graus?

Joint 2 rotacionada 90 graus

  • End effector
    • Rotação local: 0º
    • Rotação global: 90º
  • Joint 2
    • Rotação local: 90º
    • Rotação global: 90º
  • Joint 1
    • Rotação local: 0º
    • Rotação global: 0º

Notamos que rotacionar localmente um ponto, afeta a rotação global dos pontos seguintes pela mesma quantidade.

Se rotacionarmos localmente o end effector por -90 graus?

End effector rotacionada -90 graus

  • End effector
    • Rotação local: -90º
    • Rotação global: 0º
  • Joint 2
    • Rotação local: 90º
    • Rotação global: 90º
  • Joint 1
    • Rotação local: 0º
    • Rotação global: 0º

Nós conseguimos voltar a rotação global do end effector para 0º pois rotacionar localmente sempre afeta o global do mesmo ponto.

Nesse pequeno experimento já podemos começar a notar uma formula bem simples:
Rotação global = Rotação global do ponto anterior + Rotação local

Utilizando o end effector como referência:
Rotação global = 90º + (-90º) = 0º

info

No caso do primeiro ponto (joint 1), a rotação global anterior seria 0 graus.
Rotação global = 0 + Rotação local

O que aconteceria se rotacionarmos a joint 1 por 90 graus?

Joint 1 rotacionada 90 graus

  • End effector
    • Rotação local: -90º
    • Rotação global: 90º
  • Joint 2
    • Rotação local: 90º
    • Rotação global: 180º
  • Joint 1
    • Rotação local: 90º
    • Rotação global: 90º

Podemos ver que afetamos a rotação global de todos os pontos seguintes, aumentando eles por 90 graus.

Conclusion

Lembra quando falei que forward kinematics "foca em descobrir o estado da mão, dado que o braço está em certo estado". É exatamente esse somatório que nos ajuda a resolver o problema.

Vamos supor que temos 5 pontos (ponto 1 é a base):

  • Ponto 5
    • Rotação local: -90º
    • Rotação global: ?
  • Ponto 4
    • Rotação local: 0º
    • Rotação global: ?
  • Ponto 3
    • Rotação local: 180º
    • Rotação global: ?
  • Ponto 2
    • Rotação local: 0º
    • Rotação global: ?
  • Ponto 1
    • Rotação local: 90º
    • Rotação global: ?

E que nós queremos descobrir para onde cada ponto está apontando.

Se a gente começar da base, podemos ir calculando a rotação global de cada ponto usando a formula:
Rotação global = Rotação global do ponto anterior + Rotação local

Rotação global do ponto 1: 0º + 90º = 90º
Rotação global do ponto 2: 90º + 0º = 90º
Rotação global do ponto 3: 90º + 180º = 270º
Rotação global do ponto 4: 270º + 0º = 270º
Rotação global do ponto 5: 270º + -90º = 180º

note

Isto também deixa bem visível o como a rotação global de um ponto anterior afeta o próximo ponto. Por exemplo:

Somar 90 graus ao ponto 1 faria com que ponto 2 tivesse mais 90 graus...
Ponto 2 com 90 graus a mais faria com que ponto 3 tivesse mais 90 graus...
Ponto 3 com 90 graus a mais faria com que ponto 4 tivesse mais 90 graus...

  • Ponto 5
    • Rotação local: -90º
    • Rotação global: 180º
  • Ponto 4
    • Rotação local: 0º
    • Rotação global: 270º
  • Ponto 3
    • Rotação local: 180º
    • Rotação global: 270º
  • Ponto 2
    • Rotação local: 0º
    • Rotação global: 90º
  • Ponto 1
    • Rotação local: 90º
    • Rotação global: 90º

References

Thiago Lages de Alencar
var sprite := Sprite2D.new()
sprite.texture = load("res://image.png") as Texture2D
var sprite := Sprite2D.new()
var image := Image.load_from_file("user://image.png")
sprite.texture = ImageTexture.create_from_image(image) as Texture2D

Sprite2D

Sprite2D é um Node que já possui informações do que quer exibir e apenas fica responsável por administrar como exibir.

O que ele quer exiber? Texture.

Texture

Texture possui a informação daquilo que quer exibir e já foi carregado na placa de video.

O que ele quer exibir? Image.

Image

Image possui a informação daquilo que quer exibir e já foi carregado na memória RAM.

O que ele quer exibir? File.

File

File possui os bytes daquilo que quer exibir porém ainda está no HD/SSD.

load() vs load_from_file()

load()

Utilizado para carregar imagens que foram comprimidas e armazenadas juntas ao executável do jogo (as imagens que você tem que referênciar com res://).

Está função também mantém um cache das imagens carregadas, toda chamada irá retornar a mesma imagem já carregada anteriormente.

load_from_file()

Utilizado para carregar imagens novas, sem prévio conhecimento (imagens referênciadas com user:// ou que você possui os bytes em uma variável).

Está função não mantém cache, cada imagem gerada por ela irá ocupar mais espaço na memória.

References

Thiago Lages de Alencar

No post anterior declarei que busquei por um tempo uma interface gráfica (GUI) para MongoDB porém não fiquei satisfeita com nenhuma.

Neste post vou falar do desenvolvimento do Mondot, interface gráfica para o banco MongoDB.

Por favor, levar em conta que eu posso tomar decisões ruins 🤣

Reason

Em 2021, eu tinha duas coisas em mente:

  • Nenhuma GUI de Mongo me agrada
  • Quero aprender Godot

Bastou essas duas coisas para eu querer começar este projeto.
Eu não queria fazer a melhor GUI do mundo, eu queria me divertir com Godot ao mesmo tempo que resolvia um incomodo que eu tinha com GUIs.

Start

Abri o Godot, comecei a criar containers, janelas, botões, etc. Dias depois me veio a pergunta:

Como diabos eu vou me comunicar com o Mongo?

Rosto de idiota com o olhar torto para fora

Eu vou pegar o texto que o usuário escrever e fazer o que com ele?
Passar para um Node.js?
Eu vou ter que instalar Node.js na máquina da pessoa?
Como eu pego a resposta?
Eu conseguiria usar GDNative?
Como que os outros projetos fazem isto?

Agora eu precisava descobrir como faria isto acontecer.

Cheating

Colando do colega do lado

Robomongo! Também conhecido como Robo 3T.
Um projeto open-source que em 2017 foi adquirido pelos criadores do Studio 3T.

O que importa é que eu tenho um projeto para copiar estudar!

Robo 3T possui dois repositórios:

  • robomongo
    • Responsável pela interface gráfica
  • robomongo-shell
    • Responsável pela interação com o Mongo.

Importante notar que o segundo é um fork do Mongo oficial, justamente pois nele é incluido um shell interativo para se comunicar com o banco.

Isso faz sentido, já que a interface de linha de comando geralmente é a primeira coisa a ser feita para interagir com bancos. Como mais as pessoas interagiriam com o banco antes de GUI existirem? Poderia ser por código mas seria um baita trabalho cada operação.

Então era isso, tudo que eu tinha que fazer era:

  1. Incluir o shell do Mongo na minha interface gráfica
  2. Utilizar o modo iterativo do shell

Problems

Eu não tinha ideia de como fazer para incluir o shell na interface gráfica.
Decidi aceitar que passaria como o shell separado para o usuário e dei essa parte como dada.

Eu que não vou reclamar das minhas decisões idiotas no meu projeto pessoal.

Shrug face

Agora tudo que eu tinha que fazer é chamar o shell pelo Godot.
Para isto eu iria precisar usar o método OS.execute().

int execute (
String path,
PoolStringArray arguments,
bool blocking=true,
Array output=[],
bool read_stderr=false,bool open_console=false
)

O problema é que essa função possui dois comportamentos dependendo se blocking for true ou false, porém nenhum dos dois era o que eu buscava.

Outro rosto de idiota com o olhar torto para fora

blocking -> true: Godot vai pausar enquanto a saída não é escrita em output.

Não queremos isto pois uma busca pode resultar em milhares de documentos. Queremos retornar mais documentos conforme o usuário pedir por mais documentos.

blocking -> false: Godot vai continuar executando e o comando irá rodar em um processo separado. Porém não será possível recuperar o output do comando.

Nós queremos receber a saída aos poucos se estivermos falando de pesquisas que resultam em muitos documentos.

Função do Godot não é interativa, como vou usar o shell interativo?
Posso ler input e output usando algum conhecimento do sistema operacional?
Eu vou ter que conhecer bem todos sistemas operacionais?
Vou ter que descobrir como Robo 3T linka o shell?

Solution

Criar meu próprio shell.

Shell

Vantagens:

  • Utilizar a linguagem que mais estou em dia Python
    • Em outras palavras: Nenhum tempo perdido por errar algo de uma linguagem que não estou acostumado (JavaScript)
  • Utilizar a mesma linguagem com qual trabalho para fazer queryies
  • Utilizar o padrão de comunicação que eu quiser
    • No próximo tópico você vai entender o que eu quero dizer com isto

Se estivessemos falando de um produto para diversos usuários, definitivamente JavaScript seria a melhor escolha.

Para projeto pessoal com interesse em Godot? Utilizar a linguagem que mais tenho conhecimento no momento.

Communication

Shell

De todas as maneiras para duas aplicações se comunicarem, eu escolhi a mais simples.

Arquivos... :D

Em vez de quebrar a cabeça para entender como eu poderia reproduzir a interatividade do shell do Mongo, eu poderia apenas fazer com que ambos escrevessem em arquivos quando quisessem se comunicar um com o outro.

O processo em si é bem simples:

  1. Godot executa o shell, passando a query
  2. Shell escreve X documentos em um arquivo
  3. Godot solicita mais documentos

Note que você deve repetir etapa 2 e 3 até acabar os documentos ou Godot mandar parar.

Start

Primeira parte da comunicação entre shell e Mondot

  1. Godot escreve o código Python em um arquivo
    • xxxxx representa o nome do arquivo
    • O nome do arquivo é aleatório
  2. Godot executa o shell passando o caminho para o arquivo como parâmetro

Output

Segunda parte da comunicação entre shell e Mondot

  • Godot fica periodicamente conferindo se o arquivo de saída existe
    • xxxxx_out_0 representa o primeiro arquivo de saída
      • xxxxx_out_1 segundo arquivo de saída
      • xxxxx_out_2 terceiro arquivo de saída
      • etc
  • Shell escreve no arquivo de saída um JSON com o resultado da query
    • O resultado de uma query pode ser partido em diversos arquivos
      • xxxxx_out_0, xxxxx_out_1, xxxxx_out_2, ...
      • Esses arquivos são criados conforme o usuário solicita mais documentos do resultado

Input

Terceira parte da comunicação entre shell e Mondot

  • Godot escreve no arquivo de entrada solicitando mais documentos do resultado da query
  • Shell fica periodicamente conferindo se tem algo escrito no arquivo de entrada
    • xxxxx_in representa o arquivo de entrada
    • Ele lê o arquivo e remove o conteúdo do arquivo
      • Justamente para receber futuras solicitações pelo mesmo arquivo

Shell

O que o meu shell precisa fazer?

  • Receber o código do usuário
  • Executar o código do usuário
  • Iterar sobre o resultado do código
    • Iterar em partes (pegando X documentos por vez)

Em outras palavras, eu preciso acoplar o código do usuário ao código do shell.

Código do usuário sendo acoplado no meio do código do shell

Note que existem operações que meu shell irá fazer antes e após a execução do código do usuário. Por causa disso, o desenho mostra o código no meio do código shell.

O plano era receber um código simples do usuário, parecido com o do shell do Mongo:

db.test.find({})

Attempt 1

eval(expression, globals=None, locals=None)

Bom: A função faz o parser e avalia a expressão, retornando o resultado dela.

result = eval("""
db.test.find({})
""")

Ruim: Precisa ser uma expressão. Uma ou mais declarações não funcionam.

# Error
result = eval("""
db.test.find({})
db.test.find({})
""")

Attempt 2

exec(object, globals=None, locals=None, /, *, closure=None)

Bom: Executa múltiplas declarações.

exec("""
db.test.find({})
db.test.find({})
""")

Ruim: Cada declaração pode ter ou não um resultado, então essa função não retornar nada.

# result is None
result = exec("""
db.test.find({})
db.test.find({})
""")

Problem

Nas tentativas acima, para obter o resultado desejado eu teria que alterar a string. Porém, isto apresenta grande risco da alteração dar erro pois cada caso pode requer uma alteração diferente.

No final eu seria forçado a ler o código, entende-lo para depois alterar sem grande chance de erros. Mas sabe quem já faz isso de ler e entender código? O compilador.

AST

Desenho de uma árvore para representar AST

A compilação de uma linguagem envolve diversas etapas. Uma delas envolve gerar uma árvore sintática abstrata, uma árvore que garante que os objetos estão ligados corretamente.

Podemos usar este conhecimento para compilar parcialmente o código do usuário. Por exemplo, o seguinte código:

for x in db.test.find({}):
db.test.update({"_id": x["_id"]}, {"code": x["code"] + 1})
db.test.find({})

Gera uma árvore com a seguinte raiz:

Simples AST

Note que eu só mostrei o início da árvore pois todo o resto é irrelevante para nós.
Não vamos alterar nada que esteja a fundo do código do usuário, apenas na raiz.

Com esse conhecimento em mão, podemos finalmente alterar o código do usuário!

info

Utilizei um módulo do próprio Python chamado ast para analisar e reestruturar a AST.

Rewriting code

Vamos criar um código inútil apenas para usar como exemplo:

# User code
doc = db.test.find_one({})
db.test.update_one({}, {"valid": True})
db.test.find({})

A idéia é transformar este código em uma função que o shell poderá chamar e receber de volta o valor da última expressão. Em outras palavras, queremos isto:

def code():
# User code
doc = db.test.find_one({})
db.test.update_one({}, {"valid": True})
return db.test.find({})

Vamos ao passo a passo de como obter isto:

  1. Análisar o código do usuário com AST
  2. Inserir o conteúdo do módulo dentro de uma função pré montada
  3. Encapsular a última expressão em um return
    • Apenas fazer isto se for uma expressão que retorna valor

O video seguinte demonstra a transformação que está sendo feita de certa forma.

Conclusion

Com isto eu consegui preparar justamente uma interface gráfica para o Mongo em Godot. Esse foi meu projeto de 2021!


Em 2023 eu decidi dar uma refatorada e criei uma segunda versão. Mas toda essa base vista do Mondot ainda é a mesma!


Agora já estou cansado de falar deste projeto hahahaha, foi bom enquanto durou.

References

Thiago Lages de Alencar

Na primeira parte nós focamos exclusivamente em tiles que precisavam apenas satisfazer uma relação com os adjacentes. Porém existe casos onde relação com as diagonais também é importante.

Vamos organizar em 3 tipos:

  • Sides (visto anteriormente)
    • O foco era casar a adjacente.
  • Corners (será visto)
    • O foco vai ser casar duas adjacentes e uma diagonal.
  • Corners and Sides (será visto)
    • O foco vai ser resolver corners e sides.

A seguinte imagem desmonstra como cada tipo deve casar:

Comparação entre os 3 tipos de tiles

Tirando isto a lógica principal de Wang tiles permanece, ou seja, não precisamos falar dos mesmos assuntos vistos na primeira parte pois você só precisa adaptar a maneira de casar tiles.

Porém iremos mostrar para cada um dos tipos:

  • Todos os possiveis tiles
  • Mínimo de tiles considerando rotação e reflexão

Sides

Todos os possiveis tiles para &quot;sides&quot;

16 tiles

Minimal

O mínimo tiles para &quot;sides&quot;

6 tiles


Corners

Todos os possiveis tiles para &quot;corners&quot;

16 tiles

Minimal

Todos os possiveis tiles para &quot;corners&quot;

6 tiles


Corners and Sides

note

Clique nas imagens para abrir em outra janela e depois de bastante zoom.
E lembre que para restar o zoom existe o atalho Ctrl+0.

Todos possiveis tiles para &quot;corners and sides&quot;

256 tiles

Minimal

Todos os possiveis tiles para &quot;corners&quot;

51 tiles


References