Raycasting
Use raycasting para traçar uma linha no espaço e consultar colisões com entities na scene.
Raycasting é uma ferramenta fundamental no desenvolvimento de jogos. Com raycasting, você pode traçar uma linha imaginária no espaço e verificar se alguma Entity é interceptada por essa linha. Isso é útil para calcular linhas de visão, trajetórias de balas, algoritmos de pathfinding e muitas outras aplicações.
Quando um jogador pressiona o botão do pointer, ou o botão primário ou secundário, um ray é traçado da posição do jogador na direção para a qual ele está olhando, veja eventos de botão para mais detalhes sobre isso. Este documento aborda como traçar um ray invisível a partir de qualquer posição e direção arbitrárias, independentemente das ações do jogador, o que você pode usar em muitos outros cenários.
Observe que raycasts só atingem objetos com colliders. Então, se você quiser detectar hits de ray contra um modelo 3D, ou:
O modelo deve conter collider meshes.
O
GLTFContainerdeve ser configurado para usar a geometria visível com collision masks.Adicione um componente MeshCollider.
Também é uma boa prática atribuir collision layers personalizadas a modelos 3D, para que os rays só precisem calcular colisões contra as Entities relevantes, em vez de contra tudo o que tem um collider.
Criar um ray
Todos os rays têm um ponto de origem e uma direção. O ponto de origem é baseado na posição de uma Entity, usando os valores do componente Transform da Entity. A direção de um ray pode ser definida de 4 formas diferentes:
local: Uma direção relativa à direção para a frente da Entity, afetada também pela transformação de quaisquer Entities pai. Isso é útil para detectar obstáculos à frente de veículos, respeitando sua orientação.
global: Ignora a rotação da Entity e aponta para uma direção como se a rotação da Entity fosse 0. Isso é útil, por exemplo, para apontar sempre para baixo.
alvo global: Traça uma linha entre a posição da Entity e uma posição global alvo na cena. Ignora a rotação da Entity. Útil, por exemplo, para criar jogos de tower defense, onde a torre de cada estrutura pode apontar para uma coordenada precisa no espaço.
Entity alvo: Traça uma linha entre a posição da Entity e a posição de uma segunda Entity alvo. Ignora a rotação de ambas as Entities.
O código a seguir cria um raycast com uma direção local:
const myEntity = engine.addEntity()
Transform.create(myEntity, {
position: Vector3.create(4, 1, 4),
})
raycastSystem.registerLocalDirectionRaycast(
{
entity: myEntity,
opts: { direction: Vector3.Forward() },
},
function (raycastResult) {
// função de callback
}
)Use as seguintes funções para criar raycasts fornecendo a direção de diferentes formas:
raycastSystem.registerLocalDirectionRaycast(): cria um raycast com uma local direçãodirectionespera umVector3que descreve um vetor relativo à Entity e à sua rotação (por exemplo,Vector3.Forward()acabaria usando o vetor forward do Transform da Entity)raycastSystem.registerGlobalDirectionRaycast(): cria um raycast com uma global direçãodirectionespera umVector3que descreve a direção global.raycastSystem.registerGlobalTargetRaycast(): cria um raycast com uma direção definida por uma alvo global posiçãotargetespera umVector3que descreve uma posição global na cena.raycastSystem.registerTargetEntityRaycast(): cria um raycast com uma direção definida em direção a uma Entity alvo posiçãotargetEntityo campo espera uma referência a uma Entity; a posição desta Entity será usada como alvo do ray.
Os seguintes campos opcionais estão disponíveis ao criar um ray com qualquer um dos métodos acima:
maxDistance: number para definir o comprimento com que este ray será traçado. Se não for definido, o padrão é 16 metros.queryType: RaycastQueryType valor de enum, para definir se o ray retornará todas as Entities atingidas ou apenas a primeira. As seguintes opções estão disponíveis:RaycastQueryType.RQT_HIT_FIRST: (padrão) retorna apenas a primeira Entity atingida, começando do ponto de origem.RaycastQueryType.RQT_QUERY_ALL: retorna todas as Entities atingidas, da origem até a distância máxima do ray.
originOffset: Em vez de iniciar o raycast na posição de origem da Entity, adicione um offset para iniciar a consulta a partir de uma posição relativa. Você pode, por exemplo, usar um pequeno offset para evitar que o ray colida com o próprio collider da Entity. Se não for definido, o padrão éVector3.Zero().collisionMask: Detecta colisões apenas com determinadas collision layers. Use isso junto com uma collision layer personalizada, ou para detectar apenas a physics layer ou a pointer events layer. Veja collision layers. Se não for definido, a camada padrão usada éColliderLayer.CL_PHYSICS.continuous: Se verdadeiro, continuará executando uma consulta de raycast em cada frame. Se falso, o ray será usado apenas no frame atual. Se não for definido, o padrão é falso.Ao definir a direção com uma direção local ou glocal, o
directioncampo tem como padrãoVector3.Forward().Ao definir a direção com um alvo global, o
targetcampo tem como padrãoVector3.Zero().Ao definir a direção com um alvo de Entity, o
targetEntitycampo tem como padrão a Entity raiz da cena, localizada emVector3.Zero().
📔 Nota: O continuous property deve ser usado com cautela, pois executar uma consulta de raycast em cada frame pode ser muito caro em termos de performance. Sempre que possível, use um sistema (ou a interval função na biblioteca Utils) para executar consultas de raycast em um intervalo regular mais espaçado, veja raycasting recorrente.
A seguir estão exemplos usando cada um dos quatro métodos para determinar a direção do ray:
📔 Nota: raycastSystem, RaycastQueryType e ColliderLayer devem ser importados via
import { raycastSystem, RaycastQueryType, ColliderLayer } from "@dcl/sdk/ecs"
Veja Imports para saber como lidar facilmente com isso.
Resultado do raycast
A função de callback que trata o raycast recebe um objeto contendo dados sobre o próprio ray e quaisquer Entities que tenham sido atingidas.
globalOrigin: A posição onde o ray se originou, relativa à cena.direction: A direção global para a qual o ray estava apontando, como umVector3.hits: Um array com um objeto para cada Entity que foi atingida. Se não houver Entities atingidas, este array fica vazio. Se o raycast usouRaycastQueryType.RQT_HIT_FIRST, este array conterá apenas um objeto.
Cada objeto no hits array inclui:
entityId: Número de Id da Entity que foi atingida pelo ray.meshName: String com o nome interno do mesh específico no modelo 3D que foi atingido. Isso é útil quando um modelo 3D é composto por vários meshes.position: Vector3 para a posição onde o ray intersectou a Entity atingida (relativa à cena)length: Comprimento do ray desde sua origem até a posição onde ocorreu o hit contra a Entity.normalHit: Vector3 para a normal da superfície atingida no espaço mundial.globalOrigin: Vector3 para a posição onde o ray se origina (relativa à cena)direction: A direção global para a qual o ray estava apontando, como umVector3.
O exemplo a seguir percorre as Entities que foram atingidas:
📔 Nota: Você pode obter um resultado de raycast ao atingir uma Entity em uma cena diferente.
Tratar Entities atingidas
Quando você obtém um resultado de raycast que atingiu uma Entity, você pode usar o entityId para interagir com a Entity e seus components. Uma Entity é nada mais do que um número, então o entityId próprio valor pode ser interpretado como um Entity tipo.
Collision layers
É uma boa prática verificar colisões apenas contra Entities relevantes, para tornar a cena mais performática. O collisionMask campo permite listar apenas collision layers específicas — a camada de physics (paredes e pisos da cena), a camada do pointer (pointer events), as camadas dos players (avatars), ou 8 camadas personalizadas que você pode atribuir livremente. Veja collision layers.
Por padrão, o collisionMask campo está definido como ColliderLayer.CL_PHYSICS. Você pode alterar este valor para listar outras camadas, ou combinar várias com o | separador.
Raycasting recorrente
Ao usar as funções do raycastSystemraycastSystem continuous campo para true para executar uma consulta e a função de callback em cada tick do loop do jogo.
O exemplo a seguir continuará executando a consulta de raycast a partir deste ponto em diante
📔 Nota: O continuous property deve ser usado com cautela, pois executar uma consulta de raycast em cada frame pode ser muito caro em termos de performance.
Quando não for mais necessário, remova quaisquer raycasts recorrentes. Para isso, você deve usar raycastSystem.removeRaycasterEntity.
Sempre que possível, use um sistema (ou a interval função na biblioteca Utils) para executar consultas de raycast em um intervalo regular mais espaçado, como apenas uma vez por segundo, ou a cada quinto de segundo.
O exemplo acima executa um raycast recorrente a cada 0,1 segundos. Ele usa um componente de timer e a propriedade dt de um system para marcar esses intervalos de forma uniforme. Ele também inclui um cubo que oscila para cima e para baixo, controlado por outro system, para entrar e sair do caminho do ray. dt propriedade para sincronizar isso de forma uniforme. Ele também inclui um cubo que oscila para cima e para baixo, controlado por outro system, para entrar e sair do caminho do ray.
Raycasts via um system
Outra forma de executar raycasts recorrentes é executá-los de dentro da função recorrente de um system. Isso permite que você tenha muito mais controle sobre quando e como eles funcionam. Em vez de registrar uma função de callback, você pode executar uma consulta de raycast com raycastSystem.registerRaycast e então verificar os dados retornados por essa operação, tudo dentro da função do system.
Observe que, como o raycast é executado em um system, o resultado só estará disponível no tick seguinte, exigindo duas execuções do system. Uma para registrar o raycast para o frame seguinte, e o frame seguinte para processar seu resultado.
Colidir com o jogador
Você pode detectar avatars diretamente com raycasts incluindo qualquer uma das collision layers de avatar na mask:
ColliderLayer.CL_PLAYER: corresponde a qualquer avatar — o jogador local E qualquer outro jogador renderizado na cena.ColliderLayer.CL_MAIN_PLAYER: corresponde apenas ao jogador local (main).
Ambas as layers podem ser combinadas ou usadas independentemente. O collisionMask padrão para um raycast é CL_PHYSICS, que NÃO atinge avatars — você precisa habilitar isso explicitamente.
Quando um raycast atinge o jogador local, o hit.entityId é engine.PlayerEntity. Hits em avatars remotos não carregam um id de Entity local da cena (o campo é 0) porque o jogador remoto não é uma Entity no mundo da sua cena — mas o hit ainda é reportado com sua position, length, normalHite outros dados geométricos.
Raycasts a partir do jogador
Para traçar um ray a partir da posição do jogador na direção para a qual a câmera está apontando, você pode traçar um ray usando a câmera ou o avatar Reserved entities.
O exemplo a seguir traça um ray da posição da câmera do jogador para a frente, usando a engine.CameraEntity Entity.
📔 Nota: Tenha em mente que, em 3rd person, o cursor pode no futuro não se comportar da mesma forma que em 1st person. Recomenda-se usar isso apenas se o jogador estiver em 1st person.
Raycast a partir da posição do cursor
Você também pode traçar um ray a partir da posição do cursor do jogador para dentro do mundo 3D. Isso pode ser usado para arrastar objetos, shooters etc.
Neste exemplo, detectamos quando o jogador pressiona a tecla E e então traçamos um ray da posição do cursor para dentro do mundo 3D. Em seguida, verificamos se o ray atingiu alguma Entity e, se sim, fazemos algo com ela.
Sintaxe avançada
Criar um componente raycast
Um componente Raycast descreve o raio invisível usado para consultar entities que se intersectam. O raio é traçado a partir da posição da Entity, conforme definido pelo componente Transform e afetado pelo de quaisquer entities pai. A direção pode ser definida de várias maneiras,
Os rays são definidos usando os seguintes dados:
direction: Um objeto que contém um$casecampo para selecionar o tipo de direção, e um campo adicional que dependerá desse tipo, que determina essa direção. Os valores aceitos para$case:'localDirection': Uma direção relativa à direção para a frente da Entity, também afetada pela transformação de quaisquer entities pai. Isso é útil para detectar obstáculos à frente de veículos respeitando sua orientação. A rotação é definida pelolocalDirectionque descreve uma rotação.Vector3that describes a rotation.'globalDirection': Ignora a rotação da Entity, e aponta para uma direção como se a rotação da Entity fosse 0. Isso é útil, por exemplo, para apontar sempre para baixo. A rotação é definida peloglobalDirectionque descreve uma rotação.Vector3that describes a rotation.'globalTarget': Traça uma linha entre a posição da Entity e uma posição global alvo na Scene. Ignora a rotação da Entity. Útil para criar jogos de tower defense, em que a torreta de cada torre pode apontar para uma coordenada precisa no espaço. O alvo é definido peloglobalTargetque descreve uma rotação.Vector3que descreve a posição global.'targetEntity': Traça uma linha entre a posição da Entity e a posição de uma segunda Entity alvo. Ignora a rotação de ambas as entities. O alvo é definido pelotargetEntitycampo, contendo uma referência à Entity.
maxDistance: number para definir o comprimento com que este raio será traçado.queryType: RaycastQueryType valor de enum, para definir se o ray retornará todas as Entities atingidas ou apenas a primeira. As seguintes opções estão disponíveis:RaycastQueryType.RQT_HIT_FIRST: retorna apenas a primeira Entity atingida, a partir do ponto de origem.RaycastQueryType.RQT_QUERY_ALL: retorna todas as Entities atingidas, da origem até a distância máxima do ray.
collisionMask: Detecta colisões apenas com determinadas collision layers. Use isso junto com uma collision layer personalizada, ou para detectar apenas a physics layer ou a pointer events layer. Veja collision layers. Por padrão, o valor éColliderLayer.CL_POINTER | ColliderLayer.CL_PHYSICS.originOffset: Em vez de iniciar o raycast a partir da posição de origem da Entity, adicione um offset para iniciar a consulta a partir de uma posição relativa. Por exemplo, você pode usar um pequeno offset para impedir que o raio colida com o próprio modelo 3D da Entity.continuous: Se true, continuará executando uma consulta raycast a cada frame. Se false, o raio será usado apenas no frame atual. Por padrão, este valor é false.
📔 Nota: O continuous property deve ser usado com cautela, pois executar uma consulta de raycast em cada frame pode ser muito caro em termos de performance. Sempre que possível, use um sistema (ou a interval função na biblioteca Utils) para executar consultas de raycast em um intervalo regular mais espaçado, veja raycasting recorrente.
O exemplo a seguir usa uma rotação global para determinar a direção e retorna apenas a primeira Entity atingida no frame em que o raio é enviado.
O exemplo abaixo lança um raio na direção para a frente da Entity, retornando apenas o primeiro item atingido. Ele faz isso continuamente. Também inclui um pequeno offset de 0,5 para impedir que o raio atinja o collider da própria Entity.
Este exemplo traça um raio entre duas entities. Ele retorna todas as entities atingidas entre elas.
Componente de resultados do Raycast
📔 Nota: A forma mais fácil de lidar com os resultados de raycast é usar raycastSystem, e registrar uma função de callback como parte da mesma instrução que cria o raio. ORaycastResult componente é usado internamente por essa interface, mas também é exposto para permitir uma lógica personalizada mais avançada.
Depois de criar um componente Raycast, a entity à qual este component é adicionado terá um RaycastResult component. Este component inclui informações sobre quaisquer hits do raio. Configure um system para verificar esses dados.
O RaycastResult component contém os seguintes dados:
globalOrigin: A posição onde o ray se originou, relativa à cena.direction: A direção global para a qual o ray estava apontando, como umVector3.hits: Um array com um objeto para cada Entity que foi atingida. Se não houver Entities atingidas, este array fica vazio. Se o raycast usouRaycastQueryType.RQT_HIT_FIRST, este array conterá apenas um objeto.
Cada objeto no hits array inclui:
entityId: Número de Id da Entity que foi atingida pelo ray.meshName: String com o nome interno do mesh específico no modelo 3D que foi atingido. Isso é útil quando um modelo 3D é composto por vários meshes.position: Vector3 para a posição onde o ray intersectou a Entity atingida (relativa à cena)length: Comprimento do ray desde sua origem até a posição onde ocorreu o hit contra a Entity.normalHit: Vector3 para a normal da superfície atingida no espaço mundial.globalOrigin: Vector3 para a posição onde o ray se origina (relativa à cena)direction: A direção global para a qual o ray estava apontando, como umVector3.
O exemplo abaixo mostra como você pode acessar os resultados de uma entity individual usando um system:
O próximo exemplo mostra como você pode acessar RaycastResult components de todas as entities na Scene, usando uma consulta de component.
📔 Nota: Os resultados de um raycast não chegam no mesmo tick do game loop em que você criou o raycast. Os resultados podem levar um ou vários ticks para chegar.
Em uma Scene na qual você usa vários tipos de rays para diferentes finalidades (como para path finding, verificação de line-of-sight, rastreamento de projéteis etc.), talvez você queira usar diferentes collision layerspara evitar calcular colisões irrelevantes.
Atualizado