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).

O problema

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...

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.

Mas o que isso significa?

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.

Soluções alternativas

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.

Usando RayCast3D

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)

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

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

Usando intersect_ray

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)

É 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

Adicionando um Node3D como um ponto de referência para medir a distância

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)
  • 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”.

Demo

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

demo of the proposed solutions

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: