A pegadinha na hora de medir a distância entre dois objetos. (#dev #gameDev #godot)

A pegadinha na hora de medir a distância entre dois objetos.

Overview

Então, você está construindo um jogo e quer fazer algo quando o NPC estiver ha 0.5m dedistância do jogador. Você escreve um script simples para medir a distância entre os dois objetos, e ele não funciona como esperado.

Se você tem mais experiência no Godot, provavelmente já adivinhou qual é a pegadinha. Se não, vamos nos falar sobre isso. Para este post, usarei a versão 4.2.2 estável do Godot Engine (gdscript).

Dentro do script do NPC, você coloca um script tipo este aqui:

var stopping_discance := 0.5
var distance = global_position.distance_to(other_object.global_position)
if distance < stopping_distance:
    # Faça algo, talvez dê um gift card da Starbucks para o jogador ou algo assim...
gd

O problema é que, quando você executa o jogo, o NPC nunca para, mesmo quando está bem próximo do jogador. O que está acontecendo? A questão é que o método distance_to retorna a distância entre as origens dos dois objetos, não suas superfícies. Para constar, o mesmo acontece com o método distance_squared_to.

Isso significa que a posição usada para calcular a distância é o ponto de origem, que geralmente está no centro do objeto, mas quando fazemos esse tipo de coisa, estamos pensando visualmente, ou seja, queremos a distância entre os corpos, como se o NPC estivesse parado bem na frente do jogador, queremos que a distância seja de 0,5m, não a distância entre as origens (centro do objeto).

Se é isso que você espera, então não há problema. Caso contrário, vamos ver como corrigir isso.

Na verdade, corrigir é uma palavra forte porque não é um bug, é uma feature! Só não é a feature que queremos.

Aqui estão algumas possíveis soluções alternativas, talvez uma delas se adapte às suas necessidades. Antes de passar por todas essas opções, gostaria de apontar que esta não é uma lista exaustiva, e talvez você possa usar uma dessas soluções como ponto de partida para criar a sua própria.

Então, adicionamos um RayCast3D à cena do Jogador e configuramos da seguinte forma:

  1. Desmarque a opção enabled;
  2. Deixe a transformação como está (0, 0, 0);
  3. Certifique-se de que Exclude Parent esteja marcado;

Eu posso ouvir você perguntando:

Preciso mudar o valor de target position do RayCast3D?

Não. Vamos definir isso via código, então podemos deixar como está.

Espera, por que deixar o RayCast3D no centro do objeto?

Porque se ele estiver na borda do corpo do NPC, quando estiver realmente próximo do jogador (ou outro alvo), o RayCast3D não acertará nada, então você nunca obterá distance = 0.

Aqui está um trecho de como usá-lo:

# Atualize a posição alvo do RayCast3D
ray_cast_3d.target_position = other_object.global_position - global_position
ray_cast_3d.force_raycast_update() # Como o RayCast3D está desativado, precisamos forçar a atualização.

if not ray_cast_3d.is_colliding():
    return

var collision_point = ray_cast_3d.get_collision_point()
var collision_distance = global_position.distance_to(collision_point)
gd

Isso funcionará 100% corretamente? Quase. A razão é que o RayCast3D está no centro do objeto, o que era 50% do problema.

Então, como poderíamos resolver isso também? Bem, como o ponto de colisão do ray cast 3d é a superfície (corpo) do objeto, podemos apenas adicionar um offset à distância:

# Considerando que o objeto é um CSGBox3D:
@onready var size_offset_x := size.x * 0.5

# Atualize a posição alvo do RayCast3D
ray_cast_3d.target_position = other_object.global_position - global_position
ray_cast_3d.force_raycast_update() # Como o RayCast3D está desativado, precisamos forçar a atualização.

if not ray_cast_3d.is_colliding():
    return

var collision_point = ray_cast_3d.get_collision_point()
var collision_distance = global_position.distance_to(collision_point) - size_offset_x
gd

Isso resolverá o problema e agora podemos obter a distância entre dois corpos, não suas origens, e o valor mínimo será 0.

  • Prós:
    • É simples de implementar e usar;
  • Contras:
    • Não é 100% preciso, então precisamos adicionar o offset.

Spoilers: O offset vai resolver o problema em todas as soluções que vou apresentar.

Referência: https://docs.godotengine.org/en/stable/classes/class_raycast3d.html

Outra maneira de fazer isso é usar o método intersect_ray do objeto PhysicsDirectSpaceState3D. Este tem a conveniência de não precisar adicionar um RayCast3D à cena, mas é um pouco mais complexo de usar. Na versão 4.2.2, o método requer uma instância de PhysicsRayQueryParameters3D em vez de passar os dados diretamente.

Aqui está um trecho de como usá-lo:

var space = get_viewport().world_3d.direct_space_state

var ray = PhysicsRayQueryParameters3D.new()
ray.from = global_transform.origin
ray.to = other_object.global_transform.origin
ray.exclude = [self]

var hit = space.intersect_ray(ray)

if not hit:
    return

var hit_position = hit["position"]
var collision_distance = global_position.distance_to(hit_position)
gd

É praticamente o mesmo que o RayCast3D, mas precisávamos criar uma instância de PhysicsRayQueryParameters3D em vez de passar os dados.

  • Prós:
    • Também é simples de implementar e usar;
    • Você não precisa adicionar um RayCast3D à cena;
  • Contras:
    • Também não é 100% preciso, então precisamos adicionar o offset.
    • Você tem que criar uma instância de PhysicsRayQueryParameters3D, então é menos conveniente que o RayCast3D.

(Não sei qual é melhor em termos de desempenho, ou mesmo se há uma diferença.)

Referência: https://docs.godotengine.org/en/stable/classes/class_physicsdirectspacestate3d.html

Então, isso parece uma gambiarra, mas é uma solução que funciona. Você pode adicionar um Node3D à cena do NPC e colocá-lo na borda do corpo do NPC. Então, você pode usar esse nó como referência para medir a distância entre os dois objetos.

Aqui está um trecho de como usá-lo:

# Exatamente o mesmo que medir usando o distance_to regular
# Estamos apenas mudando a parte "de".
var distance = measure_from_ref_point.global_position.distance_to(other_object.global_position)
gd
  • Prós:
    • Solução mais simples de adicionar;
  • Contras:
    • Também não é 100% preciso, resolvemos o problema do ponto “de”, mas ainda temos o problema do ponto “para”.

Aqui está uma demonstração das soluções propostas:

demo of the proposed solutions
Figure 1: demo of the proposed solutions

Nesta imagem de demonstração, observe que ambos os objetos estão lado a lado. Na legenda:

  • distance_to: Resultado do método distance_to;
  • distance_to from ref: Utilizando a solução usando um Node3D como ponto de referência;
  • distance_squared_to: Resultado do método distance_squared_to;
  • Raycast distance: Resultado de usar o método RayCast3D;
  • Raycast distance - offset: Resultado de usar o método RayCast3D com o offset;
  • Intersect Ray distance: Resultado de usar o método intersect_ray.

De todos esses métodos, o mais preciso é o RayCast3D com o offset, mas você provavelmente poderia usar o offset nos outros métodos também.

Você também pode encontrar o código-fonte desta demonstração no meu GitHub: https://github.com/brenordv/godot-distance_to-gotcha

Espero ter ajudado. :)

Traduções: