making a journey-style camera

Hi all,
I’m making a dynamic semi automated “smart” camera for my game Hana

I will try to follow the gidelines said in this GDC conference: http://gdcvault.com/play/1020460/50-Camera (while deleting some and adding new ones)

The purpose of this thread is to both share my code with you (and get better dynamic camera in games) and seek help to fix bugs.

Because it seems unreal doesn’t let you enter and exit a sequence cinematic without a cut and because features like “set view target with blend” is highly player centric I will make a Actor.
(I’m okay to rewrite it as a component if someone has very good argument and fix…)

Well, To begin with this camera I made a very first draft in blueprint but because of performance, reliability between unreal version, readability (the math code is gets very ugly with bp IMO…) and because I’m more confortable with C++ I rewrote it in C++… (if you want the bp version I can post it, just ask).

So here is what I did:


UCLASS()
class HANA_API ASmartCamera : public AActor {
    GENERATED_BODY()
private:
    ACharacter* _player;
    FRotator _lastControlRotation;
    void _autoLookAt(float, bool, bool);
    float _checkObstructions(FVector, FVector);
    FVector _autoRotateAroundPlayer(float, float, float, float);
    FVector _autoDistanceToPlayer(float, float);
    float _distance(FVector, FVector);
    FVector _rotateAroundPivotAndAxis(FVector, FVector, FVector, float);
    void _controlRotationForwarding();
    UCameraComponent* _camera;
    USphereComponent* _collider; // to avoid near clip plane problems
public:
    ASmartCamera();
    virtual void BeginPlay() override;
    virtual void Tick( float DeltaSeconds ) override;
};

no UFunction or UProperty yet but it will come…


ASmartCamera::ASmartCamera() : _lastControlRotation(0.0f, 0.0f, 0.0f),
_camera(CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"))),
_collider(CreateDefaultSubobject<USphereComponent>(TEXT("Collider"))) {
    PrimaryActorTick.bCanEverTick = true;
    _collider->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    _collider->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
    _collider->SetSimulatePhysics(true);
    _collider->SetEnableGravity(false);
    _collider->SetSphereRadius(48);
    SetRootComponent(_collider);
    _camera->SetupAttachment(_collider);
    _collider->bHiddenInGame = false; // to localise the camera in gameplay (after eject)
}

For some reason I need to rotate a forward vector “FVector(1.0, 0.0, 0.0)” but in my blueprint code it’s “FVector(-1.0, 0.0, 0.0)”, if someone can explain this weirdness.


void ASmartCamera::Tick(float DeltaTime) {
	Super::Tick(DeltaTime);
    if (_lastControlRotation != _player->GetControlRotation()) { // the player choices should always be the priority
        _lastControlRotation = _player->GetControlRotation();
        SetActorLocation(_player->GetControlRotation().RotateVector(FVector(1.0, 0.0, 0.0)) * GetDistanceTo(_player) + _player->GetActorLocation(), true, nullptr, ETeleportType::TeleportPhysics);
        _autoLookAt(DeltaTime, false, false); // need to be done after the "setLocation" or some suttering will appear at low/medium framerate (like 30)
        return;
    }
    FVector rd = GetActorForwardVector() * (_collider->GetScaledSphereRadius() + 1); // displacement to avoid the raycast conflict with _collider
    _checkObstructions(GetActorLocation() +  rd, _player->GetActorLocation()); // do I see the character ?
// "whiskers generation
    const float whiskersDiff = 10.0f;
    float RObstruction = _checkObstructions(GetActorLocation() + rd, _rotateAroundPivotAndAxis(_player->GetActorLocation(), GetActorLocation(), GetActorUpVector(), whiskersDiff));
    float LObstruction = _checkObstructions(GetActorLocation() + rd, _rotateAroundPivotAndAxis(_player->GetActorLocation(), GetActorLocation(), GetActorUpVector(), -whiskersDiff));
    FVector loc = _autoRotateAroundPlayer(LObstruction, RObstruction, DeltaTime, 20); // rotating the camera automatically when detecting a potential obstacle

    const float maxDistance = 1000;
    const float minDistance = 500;
    loc += _autoDistanceToPlayer(maxDistance, minDistance); // automatically make the camera follow the player if it's too far, or go backward if too near (automatic wall avoidance isn't implemented yet)
    SetActorLocation(loc, true, nullptr, ETeleportType::TeleportPhysics);
    _autoLookAt(DeltaTime, false, false);
    _controlRotationForwarding(); // update the control of the player because the camera has been moved without its input
}


void ASmartCamera::_autoLookAt(float deltaTime,  bool Centred, bool thirdOnLeft) {
    float interpSpeed = FMath::Lerp(10, 0, UKismetMathLibrary::Dot_VectorVector((_player->GetActorLocation() - GetActorLocation()).GetUnsafeNormal(), GetActorForwardVector())); //avoid cut when changing focus point
    FRotator r = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), _player->GetActorLocation());
    if (thirdOnLeft && !Centred) { // very naive implementation of the [rule of third](https://en.wikipedia.org/wiki/Rule_of_thirds) 
        r.Yaw += 30.0f;
    } else if (!thirdOnLeft && !Centred) {
        r.Yaw += -30.0f;
    }
    SetActorRotation(FMath::RInterpTo(GetActorRotation(), r, deltaTime, interpSpeed));
}

This part work well except 2 things:

  1. It need some polish on the movement speed.
  2. All command on the player are reversed (forward goes back, left goes right etc…) I have no idea why if someone can help.
    Except the “FVector(-1.0, 0.0, 0.0)” I don’t have any difference with the BP and I don’t have this bug in BP…

Here is some of my utility function if it can help debug:


void ASmartCamera::_controlRotationForwarding() {
    _lastControlRotation = UKismetMathLibrary::MakeRotFromX((GetActorLocation() - _player->GetActorLocation()).GetUnsafeNormal());
    _player->GetController()->SetControlRotation(_lastControlRotation);
}


float ASmartCamera::_checkObstructions(FVector start, FVector end) {
    FHitResult outHit;
    if (!UKismetSystemLibrary::LineTraceSingle_NEW(GetWorld(), start, end, ETraceTypeQuery::TraceTypeQuery1, false, TArray<AActor*>(), EDrawDebugTrace::ForOneFrame, outHit, true)) { // I don't know if there is a better raycast function...
        return 0.0f;
    }
    return _distance(outHit.TraceStart, outHit.TraceEnd);
}

Thank you if you came to help, you are welcome if find it useful.

I fixed my problem, my substraction was done backward in this function :


void ASmartCamera::_controlRotationForwarding() {
    _lastControlRotation = UKismetMathLibrary::MakeRotFromX((GetActorLocation() - _player->GetActorLocation()).GetUnsafeNormal());
    _player->GetController()->SetControlRotation(_lastControlRotation);
}

here is the fix:


void ASmartCamera::_controlRotationForwarding() {
    _lastControlRotation = UKismetMathLibrary::MakeRotFromX((_player->GetActorLocation() - GetActorLocation()).GetUnsafeNormal());
    _player->GetController()->SetControlRotation(_lastControlRotation);
}

My next step will be to handle the corridor cases:
automatically and smoothly center the camera when entering
automatically and smoothly coming closer when it’s necessary (corridor angle, player rotating the camera etc…)

I will also make the camera slowing down when it’s close to a wall (as if the walls were repulsive magnets).

I fixed, added and refactored a lot of things

Fixed:

  • the player are reversed
  • jittering in tiny environment
  • distance handling is now done in 2D (the player can’t go under the camera)
  • Acceleration variations of the rotation when an object is detected is now handled correctly
  • Rule of the Third is now handled properly as long as the player is on the same level as the camera (see known bugs)

Added:

  • Automatically and smoothly going closer from the player when going
  • The camera slow down then it’s close to an object
  • Accelerating the rotation then the player is out of the view
  • modifying the maximum distance is now handled with smoothness

Known Bugs:

  • Higher the camera is more centred the player is.
  • The player can come closer than the minimal distance when the camera has a wall behind it
  • When the player hit the camera the physics mess with the camera
  • It is possible to trap the camera behind a wall (I still don’t know what to do in that case)
  • When the camera automatically decrease the minimal distance it never increase it back (again, I don’t know how to be sure I can)

here is my new code:



private:
    ACharacter* _player;
    FRotator _lastControlRotation;
    UCameraComponent* _camera;
    USphereComponent* _collider;
    float _maxDistance;
    float _minDistance;
    void _autoLookAt(float, bool, bool);
    float _checkObstructions(FVector, FVector, FVector* = nullptr);
    FRotator _safetyMagnet(FRotator);
    FVector _autoRotateAroundPlayer(float);
    FVector _autoDistanceToPlayer(float);
    float _distance(FVector, FVector);
    FVector _rotateAroundPivotAndAxis(FVector, FVector, FVector, float);
    void _controlRotationForwarding();

public:
    ASmartCamera();
    virtual void BeginPlay() override;
    virtual void Tick( float DeltaSeconds ) override;


ASmartCamera::ASmartCamera() : _lastControlRotation(0.0f, 0.0f, 0.0f),
_camera(CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"))),
_collider(CreateDefaultSubobject<USphereComponent>(TEXT("Collider"))),
_maxDistance(900),
_minDistance(200) {
    PrimaryActorTick.bCanEverTick = true;
    _collider->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    _collider->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Block);
    _collider->SetSimulatePhysics(true);
    _collider->SetEnableGravity(false);
    _collider->SetSphereRadius(24);
    SetRootComponent(_collider);
    _camera->SetupAttachment(_collider);
    _camera->bConstrainAspectRatio = true;
    _collider->bHiddenInGame = false;
}


void ASmartCamera::Tick(float DeltaTime) {
	Super::Tick(DeltaTime);
    FVector loc;
    if (_lastControlRotation != _player->GetControlRotation()) {
        FRotator n = _player->GetControlRotation() - _lastControlRotation;
        n = _safetyMagnet(n);
        loc = _rotateAroundPivotAndAxis(GetActorLocation(), _player->GetActorLocation(), GetActorRightVector(), n.Pitch);
        loc = _rotateAroundPivotAndAxis(loc, _player->GetActorLocation(), GetActorUpVector(), n.Yaw);
        loc = _rotateAroundPivotAndAxis(loc, _player->GetActorLocation(), GetActorForwardVector(), n.Roll);
    } else {
        loc = _autoRotateAroundPlayer(DeltaTime);
    }
    
    loc += _autoDistanceToPlayer(DeltaTime);
    SetActorLocation(loc, true, nullptr, ETeleportType::TeleportPhysics);
    _autoLookAt(DeltaTime, false, false);
    _controlRotationForwarding();
}


FRotator ASmartCamera::_safetyMagnet(FRotator wanted) {
    FVector base;
    wanted.Normalize();
    if (wanted.Pitch > 0.0f) {
        base = GetActorUpVector();
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, "up");
    } else {
        base = -GetActorUpVector();
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, "Down");
    }
    FVector d = base * (_collider->GetScaledSphereRadius() + 1);
    FVector ex = base * (_collider->GetScaledSphereRadius() + 100);
    
    wanted.Pitch *= _checkObstructions(GetActorLocation() + d, GetActorLocation() + ex);
    
    if (wanted.Yaw < 0.0f) {
        base = GetActorRightVector();
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, "Right");
    } else {
        base = -GetActorRightVector();
        GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, "Left");
    }
    
    d = base * (_collider->GetScaledSphereRadius() + 1);
    ex = base * (_collider->GetScaledSphereRadius() + 100);
    wanted.Yaw *= _checkObstructions(GetActorLocation() + d, GetActorLocation() + ex);
    
    GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Green, FString::SanitizeFloat(wanted.Yaw));
    return wanted;
}


void ASmartCamera::_autoLookAt(float deltaTime, bool Centred, bool thirdOnLeft) {
    float interpSpeed = FMath::Lerp(5, 0, UKismetMathLibrary::Dot_VectorVector((_player->GetActorLocation() - GetActorLocation()).GetUnsafeNormal(), GetActorForwardVector()));
    FRotator r = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), _player->GetActorLocation());
    if (thirdOnLeft && !Centred) {
        r.Yaw += 25.0f;
    } else if (!thirdOnLeft && !Centred) {
        r.Yaw += -25.0f;
    }
    SetActorRotation(FMath::RInterpTo(GetActorRotation(), r, deltaTime, interpSpeed));
}


FVector ASmartCamera::_autoDistanceToPlayer(float deltaTime) {
    FVector p = _player->GetActorLocation();
    FVector c = GetActorLocation();
    c.Z = 0.0f;
    p.Z = 0.0f;
    float d = _distance(p, c);
    FVector f = GetActorForwardVector();
    f.Z = 0.0f;
    static float _currentMax = _maxDistance;
    _currentMax = FMath::FInterpTo(_currentMax, _maxDistance, deltaTime, 5);
    float x;
    if (d > _currentMax) {
        x = _currentMax;
    } else if (d < _minDistance) {
        x = _minDistance;
    } else {
        return FVector(0.0, 0.0, 0.0);
    }
    return (d - x) * f;
}


FVector ASmartCamera::_autoRotateAroundPlayer(float deltaTime) {
    const float whiskersDiff = 10.0f;
    FVector rd = GetActorForwardVector() * (_collider->GetScaledSphereRadius() + 1);
    FVector lImpact;
    FVector rImpact;
    float rObstruction = _checkObstructions(GetActorLocation() + rd, _rotateAroundPivotAndAxis(_player->GetActorLocation(), GetActorLocation(), GetActorUpVector(), whiskersDiff), &rImpact);
    float lObstruction = _checkObstructions(GetActorLocation() + rd, _rotateAroundPivotAndAxis(_player->GetActorLocation(), GetActorLocation(), GetActorUpVector(), -whiskersDiff), &lImpact);
    float obs = FMath::Min(lObstruction, rObstruction);
    FVector Imp = rImpact;
    if (lObstruction < rObstruction) {
        //GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::White, "check");
        Imp = lImpact;
        deltaTime *= -1;
    }

    float speed = 1.0f;
    speed *= FMath::Clamp((1.0f - (obs - 0.8f)) * 5.0f, 0.0f, 1.0f);

    if (_checkObstructions(GetActorLocation() +  rd, _player->GetActorLocation()) < 0.85) {
        speed = 5.0f;
    }
    if ( lObstruction < 1.0f && rObstruction < 1.0f) {
        FHitResult outHit;
        
        if (!UKismetSystemLibrary::LineTraceSingle_NEW(GetWorld(), _player->GetActorLocation(), Imp, ETraceTypeQuery::TraceTypeQuery1, false, TArray<AActor*>(), EDrawDebugTrace::None, outHit, true)) {
            speed = 0.0f;
        }
        
        _maxDistance = FMath::Clamp(_distance(GetActorLocation(), outHit.ImpactPoint), _minDistance + 5.0f, 1200.0f);
        speed = 0.0f;
    }
    FVector x = _rotateAroundPivotAndAxis(GetActorLocation(), _player->GetActorLocation(), GetActorUpVector(), FMath::Lerp(70, 0, obs) * speed * deltaTime);
    return x;
}

I hope that is useful to someone.
I’m open to any advice, idea etc… dynamic cameras seem to be a pretty uncharted territory.

My next step will be to handle:

  • Resident evil style camera toggle
  • Find a solution to smoothly go in and out a Sequence cinematic
  • Find a solution when the camera is trapped behind the wall
  • Automatic rotation to show possible point of interests in the scene

PS: I didn’t posted my full code, if you want my other “utility functions” just ask

Keen to get my hands on those blueprints, just to understand it a bit more.

Just a tip: Wouldn’t it be easier to maintain a public repo and describe new features here + put up a link instead of copy&pasting the entire code?

@queelaga I do it here for 2 reason:

  1. For now this camera is 100% part of a game I’m making and the code of this game is private (at least for now).
  2. I don’t really know what I’m doing and there is only a few (public) research in that field so this thread is more about thinking with the community than just posting my code with new features… Unfortunately there isn’t much guys wanting to help build a good dynamic third person camera system here so it made this thread look like the “poor man git” =/
    @Newsboy maybe in a month I will open the full code of this without the dependencies to my character (or posting it on the marketplace, I don’t know yet)

can yo share the blue print?

There is no blueprint, everything is in C++. The design is ‘you drop it in the level and it’s ready to use’

In addition there is still a lot of edge cases that make it still experimental.

I stopped updating here because I added a bit of dependency to the rest of my game. when it become usable and i deleted de dependency I will open a git or something to share it yes… But don’t expect to see this comming soon, I have A LOT of work that isn’t this camera system