Como restringir o movimento do jogador à superfície de um objeto 3D usando o Unity?

16

Estou tentando criar um efeito semelhante ao de Mario Galaxy ou Geometry Wars 3, onde o jogador caminha pelo "planeta" a gravidade parece se ajustar e eles não caem da borda do objeto, como faria se a gravidade foi fixado em uma única direção.

insira a descrição da imagem aqui
(fonte: gameskinny.com )

Geometry Wars 3

Consegui implementar algo parecido com o que estou procurando usando uma abordagem em que o objeto que deveria ter a gravidade atraia outros corpos rígidos, mas usando o mecanismo de física incorporado para o Unity, aplicando movimento com AddForce e coisas do gênero, Eu simplesmente não conseguia fazer o movimento parecer certo. Não consegui fazer com que o jogador se movesse rápido o suficiente sem começar a voar para fora da superfície do objeto e não consegui encontrar um bom equilíbrio de força aplicada e gravidade para acomodar isso. Minha implementação atual é uma adaptação do que foi encontrado aqui

Eu sinto que a solução provavelmente ainda usaria a física para aterrar o jogador no objeto se ele deixasse a superfície, mas uma vez que o jogador tenha sido aterrado, haveria uma maneira de encaixar o jogador na superfície e desligar a física e controlar o jogador por outros meios, mas não tenho muita certeza.

Que tipo de abordagem devo adotar para encaixar o player na superfície dos objetos? Observe que a solução deve funcionar no espaço 3D (em oposição ao 2D) e deve poder ser implementada usando a versão gratuita do Unity.

SpartanDonut
fonte
Eu nem pensei em procurar andar nas paredes. Vou dar uma olhada e ver se isso ajuda.
SpartanDonut
1
Se essa pergunta for especificamente sobre como fazer isso no 3D no Unity, ela deverá ficar mais clara com as edições. (Não seria uma cópia exata do que existente então.)
Anko
Esse também é meu sentimento geral - vou ver se consigo adaptar essa solução ao 3D e postá-la como resposta (ou se alguém puder me derrotar no soco, também estou bem). Vou tentar atualizar minha pergunta para ficar mais claro sobre isso.
perfil completo de Spartan
Potencialmente útil: youtube.com/watch?v=JMWnufriQx4
ssb

Respostas:

7

Consegui realizar o que eu precisava, principalmente com a ajuda deste post do blog para a peça de quebra-cabeça da superfície e tive minhas próprias idéias para o movimento do jogador e a câmera.

Como encaixar o player na superfície de um objeto

A configuração básica consiste em uma esfera grande (o mundo) e uma esfera menor (o jogador), ambas com coletores de esferas ligados a eles.

A maior parte do trabalho que está sendo realizado foi nos dois métodos a seguir:

private void UpdatePlayerTransform(Vector3 movementDirection)
{                
    RaycastHit hitInfo;

    if (GetRaycastDownAtNewPosition(movementDirection, out hitInfo))
    {
        Quaternion targetRotation = Quaternion.FromToRotation(Vector3.up, hitInfo.normal);
        Quaternion finalRotation = Quaternion.RotateTowards(transform.rotation, targetRotation, float.PositiveInfinity);

        transform.rotation = finalRotation;
        transform.position = hitInfo.point + hitInfo.normal * .5f;
    }
}

private bool GetRaycastDownAtNewPosition(Vector3 movementDirection, out RaycastHit hitInfo)
{
    Vector3 newPosition = transform.position;
    Ray ray = new Ray(transform.position + movementDirection * Speed, -transform.up);        

    if (Physics.Raycast(ray, out hitInfo, float.PositiveInfinity, WorldLayerMask))
    {
        return true;
    }

    return false;
}

o Vector3 movementDirection parâmetro é exatamente o que parece, a direção que vamos mover nosso player nesse quadro, e calcular esse vetor, embora tenha sido relativamente simples neste exemplo, foi um pouco complicado para eu descobrir no começo. Mais adiante, mas lembre-se de que é um vetor normalizado na direção em que o jogador está movendo esse quadro.

Avançando, a primeira coisa que fazemos é verificar se um raio, originário da posição hipotética futura direcionada aos vetores inativos dos jogadores (-transform.up), atinge o mundo usando o WorldLayerMask, que é uma propriedade pública do script LayerMask. Se você quiser colisões mais complexas ou várias camadas, precisará criar sua própria máscara de camada. Se o raycast atingir com sucesso algo, o hitInfo é usado para recuperar o normal e o ponto de impacto para calcular a nova posição e rotação do player, que deve estar no objeto. Pode ser necessário compensar a posição do jogador, dependendo do tamanho e da origem do objeto do jogador em questão.

Finalmente, isso realmente só foi testado e provavelmente só funciona bem em objetos simples, como esferas. Como a postagem do blog em que baseei minha solução, sugere, você provavelmente desejará executar vários raycasts e calculá-los como média para sua posição e rotação para obter uma transição muito mais agradável ao se deslocar em terrenos mais complexos. Também pode haver outras armadilhas em que não pensei neste momento.

Câmera e Movimento

Uma vez que o jogador estava grudado na superfície do objeto, a próxima tarefa a ser abordada era o movimento. Inicialmente, eu comecei com um movimento em relação ao jogador, mas comecei a encontrar problemas nos polos da esfera, onde as direções mudavam repentinamente, fazendo com que meu jogador mudasse rapidamente de direção várias vezes, sem me deixar passar pelos polos. O que acabei fazendo foi fazer meus jogadores se moverem em relação à câmera.

O que funcionou bem para minhas necessidades foi ter uma câmera que seguisse estritamente o jogador com base apenas na posição do jogador. Como resultado, mesmo que a câmera estivesse tecnicamente girando, pressionar para cima sempre movia o player em direção à parte superior da tela, para baixo em direção à parte inferior e assim por diante com a esquerda e a direita.

Para fazer isso, o seguinte foi executado na câmera em que o objeto alvo era o jogador:

private void FixedUpdate()
{
    // Calculate and set camera position
    Vector3 desiredPosition = this.target.TransformPoint(0, this.height, -this.distance);
    this.transform.position = Vector3.Lerp(this.transform.position, desiredPosition, Time.deltaTime * this.damping);

    // Calculate and set camera rotation
    Quaternion desiredRotation = Quaternion.LookRotation(this.target.position - this.transform.position, this.target.up);
    this.transform.rotation = Quaternion.Slerp(this.transform.rotation, desiredRotation, Time.deltaTime * this.rotationDamping);
}

Finalmente, para mover o player, alavancamos a transformação da câmera principal para que, com nossos controles para cima, para cima, para baixo, para baixo, etc. E é aqui que chamamos UpdatePlayerTransform, que obterá nossa posição ajustada ao objeto mundial.

void Update () 
{        
    Vector3 movementDirection = Vector3.zero;
    if (Input.GetAxisRaw("Vertical") > 0)
    {
        movementDirection += cameraTransform.up;
    }
    else if (Input.GetAxisRaw("Vertical") < 0)
    {
        movementDirection += -cameraTransform.up;
    }

    if (Input.GetAxisRaw("Horizontal") > 0)
    {
        movementDirection += cameraTransform.right;
    }
    else if (Input.GetAxisRaw("Horizontal") < 0)
    {
        movementDirection += -cameraTransform.right;
    }

    movementDirection.Normalize();

    UpdatePlayerTransform(movementDirection);
}

Para implementar uma câmera mais interessante, mas com os controles semelhantes aos que temos aqui, você pode implementar facilmente uma câmera que não é renderizada ou apenas outro objeto fictício para basear o movimento e, em seguida, usar a câmera mais interessante para renderizar o que você quer que o jogo pareça. Isso permitirá boas transições de câmera à medida que você percorre os objetos sem interromper os controles.

SpartanDonut
fonte
3

Eu acho que essa ideia vai funcionar:

Mantenha uma cópia do lado da CPU da malha do planeta. Ter vértices de malha também significa que você tem vetores normais para cada ponto do planeta. Em seguida, desative completamente a gravidade para todas as entidades, aplicando uma força exatamente na direção oposta a um vetor normal.

Agora, com base em qual ponto esse vetor normal do planeta deve ser calculado?

A resposta mais fácil (que eu tenho certeza que funcionará bem) é aproximar-se da mesma forma que o método de Newton : quando os objetos aparecem, você conhece todas as suas posições iniciais no planeta. Use essa posição inicial para determinar o upvetor de cada objeto . Obviamente, a gravidade estará na direção oposta (na direção down). No próximo quadro, antes de aplicar a gravidade, projete um raio da nova posição do objeto em direção ao seu downvetor antigo . Use a interseção desse raio com o planeta como a nova referência para determinar o upvetor. O raro caso do raio não atingir nada significa que algo deu muito errado e você deve mover seu objeto de volta para onde estava no quadro anterior.

Observe também que, usando esse método, quanto mais a origem do jogador for do planeta, pior será a aproximação. Por isso, é melhor usar em algum lugar ao redor dos pés de cada jogador como sua origem. Estou supondo, mas acho que usar os pés como origem também resultará em um manuseio e navegação mais fáceis do jogador.


Uma última observação: para obter melhores resultados, você pode até fazer o seguinte: acompanhar o movimento do jogador em cada quadro (por exemplo, usando current_position - last_position). Em seguida, prenda-o de movement_vectorforma que o comprimento em direção ao objeto upseja zero. Vamos chamar esse novo vetor reference_movement. Mova o anterior reference_pointpor reference_movemente use esse novo ponto como origem do traçado de raios. Depois (e se) o raio atingir o planeta, vá reference_pointpara esse ponto de impacto. Por fim, calcule o novo upvetor, a partir deste novo reference_point.

Algum código pseudo para resumir:

update_up_vector(player:object, planet:mesh)
{
    up_vector        = normalize(planet.up);
    reference_point  = ray_trace (object.old_position, -up_vector, planet);
    movement         = object.position - object.old_position;
    movement_clamped = movement - up_vector * dot (movement, up_vector);

    reference_point  += movement_clamped;
    reference_point  = ray_trace (reference_point, -up_vector, planet);
    player.up        = normal_vector(mesh, reference_point);
}
Ali1S232
fonte
2

Esta postagem pode ser útil. A essência é que você não usa os controladores de caracteres, mas cria o seu próprio usando o mecanismo de física. Em seguida, use as normais detectadas abaixo do player para orientá-las para a superfície da malha.

Aqui está uma boa visão geral da técnica. Existem muitos outros recursos com termos de pesquisa na web como "passeio pela unidade em objetos 3d mario galaxy".

Há também um projeto de demonstração da unidade 3.x que tinha um mecanismo de caminhada, com um soldado e um cachorro e em uma das cenas. Ele demonstrou andar em objetos 3D no estilo Galaxy . É chamado de sistema de locomoção por runevision.

Homem Aranha, Homem Aranha
fonte
1
À primeira vista, a demonstração não funciona muito bem e o pacote Unity apresenta erros de compilação. Vou ver se consigo fazer isso funcionar, mas uma resposta mais completa seria apreciada.
SpartanDonut
2

Rotação

Confira a resposta desta pergunta em answers.unity3d.com (realmente solicitada por mim). Citar:

A primeira tarefa é obter um vetor que se define. No seu desenho, você pode fazê-lo de duas maneiras. Você pode tratar o planeta como uma esfera e usar (object.position - planet.position). A segunda maneira é usar Collider.Raycast () e usar o 'hit.normal' que ele retorna.

Aqui está o código que ele me sugeriu:

var up : Vector3 = transform.position - planet.position;
transform.rotation = Quaternion.FromToRotation(transform.up, up) * transform.rotation;

Chame isso de atualização, e você deve fazê-la funcionar. (observe que o código está no UnityScript ).
O mesmo código em C # :

Vector3 up = transform.position - planet.position;
transform.rotation = Quaternion.FromToRotation(transform.up, up) * transform.rotation;

Gravidade

Para a gravidade, você pode usar minha "técnica de física do planeta", que descrevi na minha pergunta, mas não é realmente otimizada. Foi apenas algo que me ocorreu.
Eu sugiro que você crie seu próprio sistema para a gravidade.

Aqui está um tutorial do YouTube de Sebastian Lague . Essa é uma solução muito eficiente.

EDIT: Na unidade, vá para Editar > Configurações do projeto > Física e defina todos os valores para Gravity como 0 (ou remova o Rigidbody de todos os objetos), para evitar que a gravidade interna (que apenas puxa o jogador para baixo) entre em conflito com a solução personalizada. (Desligue isso)

Daniel Kvist
fonte
Gravitar o player no centro de um planeta (e girá-lo de acordo) funciona bem para planetas quase esféricos, mas eu posso imaginar que ele realmente dá errado para objetos menos esféricos, como aquele formato de foguete que Mario está pulando no primeiro GIF.
Anko
Você pode criar um cubo restrito a mover-se apenas pelo eixo X, por exemplo, dentro do objeto não esférico e movê-lo quando o jogador se mover, para que ele fique sempre diretamente sob o jogador, e depois puxe o jogador até o cubo. Tente.
precisa