Erstellen eines FPS-Controllers basierend auf RigidbodyC#

Ein Treffpunkt für C#-Programmierer
Anonymous
 Erstellen eines FPS-Controllers basierend auf Rigidbody

Post by Anonymous »

Ich versuche, meinen eigenen FPS-Controller auf Basis von Rigidbody zu erstellen, wobei „isKinematic“ aktiviert ist. Ich versuche auch, die gleichen Controller-Mechaniken zu reproduzieren, die in Spielen wie Amnesia, Penumbra, Portal, Half-Life usw. zu finden sind.
Die Sache ist, ich verstehe nicht, wie sie funktionieren, und meine Algorithmen haben nicht funktioniert. Wenn jemand etwas Ähnliches gemacht und erfolgreich einen Controller erstellt hat, teilen Sie mir bitte mit, wie Sie Ihren Controller implementiert haben.
Nun ein wenig darüber, wie ich versucht habe, ihn selbst zu implementieren:
1. Ich habe zwei Systeme – die Player State Machine und den „Player Motor“-Dienst. In der Zustandsmaschine rufe ich einfach die Servicemethoden auf.

Code: Select all

// Grounded state
public override void FixedTick()
{
base.FixedTick();

Vector3 inputMove = _inputReader.Move;
(Vector3 forward, Vector3 right) = _motor.GetPlayerDirs();

Vector3 desiredMove = forward * inputMove.y + right * inputMove.x;

_motor.Move(desiredMove * SPEED * Time.fixedDeltaTime);

if (!_motor.IsGrounded || _inputReader.IsJumping)
{
_stateMachine.SwitchState
();
return;
}
}

// Airborne state
public override void FixedTick()
{
base.FixedTick();

_verticalVelocity += GRAVITY * Time.fixedDeltaTime;

Vector3 inputMove = _inputReader.Move;
(Vector3 forward, Vector3 right) = _motor.GetPlayerDirs();

Vector3 offsetMove = (forward * inputMove.y + right * inputMove.x).normalized;

Vector3 downMove = Vector3.down;

_motor.Move(downMove * _verticalVelocity * Time.fixedDeltaTime + offsetMove * 2f * Time.fixedDeltaTime);

if (_motor.IsGrounded)
{
_stateMachine.SwitchState();
return;
}
}
2. Im Service ist alles viel komplizierter. Im Allgemeinen überprüfe ich, ob sich unter den Füßen Boden befindet (dies ist zum Umschalten der Zustände in der Zustandsmaschine erforderlich) und bearbeite dann den Positionsvektor des Spielers so, dass er nicht auf Oberflächen mit einer Neigung über einem bestimmten Grad klettert und zu Boden gezogen wird, wenn der Abstand zwischen Boden und Spieler akzeptabel ist.

Code: Select all

public void Move(Vector3 desiredMove)
    {
        CheckGround();

        Vector3 position = _playerRigidbody.position;
        position = MoveWithSlide(position, desiredMove);
        position = SnapToGround(position);

        _playerRigidbody.MovePosition(position);
    }
3. Ich überprüfe den Boden mit SphereCast – hier ist nichts Kompliziertes, siehe Code unten.

Code: Select all

private void CheckGround()
    {
        float halfHeight = _playerCollider.height * 0.5f;
        float radius = _playerCollider.radius;
        float dist = halfHeight - radius + PROBE_DOWN;

        Vector3 center = _playerCollider.transform.TransformPoint(_playerCollider.center);

        Ray sphereRay = new Ray(center, Vector3.down);
        bool isSupported = Physics.SphereCast(sphereRay, radius, dist,
                                              _worldMask, QTI);

        _isGrounded = isSupported;
    }
4. Die Kollisionserkennung ist komplizierter: Hier prüfe ich, ob vor dem Objekt eine Neigung vorhanden ist, und wenn ja, bewegen wir uns zum Kollisionspunkt mit ihm. Wenn die Steigung für uns ein normales Gefälle hat, prüfen wir auch, wie weit wir uns entlang dieser bewegen können, andernfalls behandeln wir sie wie eine Wand.

Code: Select all

private Vector3 MoveWithSlide(Vector3 position, Vector3 desiredMove)
    {
        float desiredDist = desiredMove.magnitude;
        Vector3 desiredDir = desiredMove / desiredDist;

        GetCapsulePoints(position, out Vector3 p1, out Vector3 p2, out float radius);

        if(!Physics.CapsuleCast(p1, p2, radius, desiredDir,
                                out RaycastHit hit,
                                desiredDist + SKIN,
                                _worldMask, QTI))
        {
            return position + desiredMove;
        }

        float travel = Mathf.Max(0f, hit.distance - SKIN);
        position += desiredDir * travel;

        Vector3 left = desiredMove - desiredDir * travel;

        Vector3 slide;

        if(CheckSlopeAngle(hit.normal))
        {
            slide = Vector3.ProjectOnPlane(left, hit.normal);
        }
        else
        {
            Vector3 horiz = Vector3.ProjectOnPlane(left, Vector3.up);

            Vector3 wallNormal = Vector3.ProjectOnPlane(hit.normal, Vector3.up);

            if (wallNormal.sqrMagnitude < EPS)
                return position;

            wallNormal.Normalize();

            slide = Vector3.ProjectOnPlane(horiz, wallNormal);
        }

        float slideDist = slide.magnitude;
        if (slideDist < EPS)
            return position;

        Vector3 slideDir = slide / slideDist;

        GetCapsulePoints(position, out Vector3 sp1, out Vector3 sp2, out float sradius);

        if (Physics.CapsuleCast(sp1, sp2, sradius, slideDir,
                                out RaycastHit shit,
                                slideDist + SKIN, _worldMask, QTI))
        {
            float md2 = Mathf.Max(0f, shit.distance - SKIN);
            return position + slideDir * md2;
        }

        return position + slide;
    }
5. Auch das Einrasten ist einfach: Ich erhalte die Kapselspitzen in einer separaten Methode (ich werde nicht näher darauf eingehen, es ist unnötig. Ich bin mir sicher, dass es richtig funktioniert), und ich werfe die Kapsel nach unten und überprüfe, ob ein Einrasten möglich ist.

Code: Select all

private Vector3 SnapToGround(Vector3 position)
    {
        GetCapsulePoints(position, out Vector3 p1, out Vector3 p2, out float radius);

        if (Physics.CapsuleCast(p1, p2, radius, Vector3.down,
                                out RaycastHit hit,
                                SNAP_DOWN_DIST + SKIN,
                                _worldMask, QTI)
            && CheckSlopeAngle(hit.normal))
        {
            float down = hit.distance - SKIN;
            position += Vector3.down * down;

            _isGrounded = true;
        }

        return position;
    }
Im Allgemeinen funktioniert es gut, in bestimmten Fällen treten jedoch Probleme auf. Solange wir in Spielen die Oberfläche nicht vollständig verlassen, werden wir überhaupt nicht fallen. Aber mein Controller beginnt zu „baumeln“, und das gefällt mir nicht.
Image

Ich weiß, dass das Problem in den Besetzungen liegt. Wenn ich einen normalen Raycast machen würde, würde die Oberflächennormale korrekt zurückgegeben und die Oberflächenkollision wäre dann wie in normalen Spielen. Aber ich hatte einige Probleme, an die ich mich ehrlich gesagt nicht mehr erinnern kann (sorry, ich arbeite jetzt schon seit einer Woche an diesem Controller).
Es gab die Idee, CapsuleCast und SphereCast durch Strahlen zu ersetzen, die kreisförmig verlaufen, mit dem Radius der Spielerkapsel, nach unten, und so nach dem Normalen für die Projektion suchen. Aber ich bin mir nicht sicher, ob das eine gute Idee ist.

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post