What is considered "copying engine code?"

Unreal Engine does not allow people to “steal” code from its source, and rightly so. However, they do say in their FAQ that a person can use the knowledge they gain from reading/using UE’s source for other products, even competing ones.

So, I guess my question is, what would be considered the line between “copying” code and “using knowledge gained from reading code?” So, let’s say, for instance, I wanted to create a character movement script for Unity and post it on GitHub. Then, I discover I have no idea how to implement sticking to platforms, so I look at CharacterMovementComponent to see how Epic did it. I study what they did, and figure out why it works. Then would doing a fix similar to how they fixed the problem be considered copying the code? Or would that be considered using it to learn?

Thanks.

1 Like

I think they mean CTRL-C :slight_smile:

Nobody’s going to have a problem with someone using similar concepts, but reusing large chunks of render code / game-thread is going to cause problems in the long run.

1 Like

Let me give you a more concrete example. I’ll use CharMoveComp again. Here’s the code that controls the platform movement:

// Calculate new transform matrix of base actor (ignoring scale).
const FQuatRotationTranslationMatrix OldLocalToWorld(OldBaseQuat, OldBaseLocation);
const FQuatRotationTranslationMatrix NewLocalToWorld(NewBaseQuat, NewBaseLocation);
// We need to offset the base of the character here, not its origin, so offset by half height
float HalfHeight, Radius;
CharacterOwner->GetCapsuleComponent()->GetScaledCapsuleSize(Radius, HalfHeight);

FVector const BaseOffset(0.0f, 0.0f, HalfHeight);
FVector const LocalBasePos = OldLocalToWorld.InverseTransformPosition(UpdatedComponent->GetComponentLocation() - BaseOffset);
FVector const NewWorldPos = ConstrainLocationToPlane(NewLocalToWorld.TransformPosition(LocalBasePos) + BaseOffset);

I then do something similar in a Unity script:

// Get our current position relative to the floor's old transform
Vector3 oldFloorPos = lastFloorTransform.InverseTransformPoint(transform.position);
// Determine what our world position would be if we were on the same spot relative to the floor's current transform
Vector3 newFloorPos = currentFloorTransform.TransformPoint(oldFloorPos);
// Find out how much we need to move to that new relative position
Vector3 floorAnchorPositionDelta = (newFloorPos - transform.position);

_characterController.Move(floorAnchorPositionDelta);

Would this qualify as something I can use and share?

That looks ok to me. Although, I can’t speak for Epic on a legal level.

I think you could even say, ‘this is a Unity recode of the Unreal character movement’, and not have any problems.

1 Like

I wish they gave easier access to their official support for such matters. Anyway, thanks for your answers. :smiling_face_with_three_hearts:

1 Like

So, I’ve pretty much finished the script I was working on. I’d like opinions to see if this code is too similar to CMP. For reference, I read though the CMP code, completely reverse-engineered how it works, and remade it using some similar methods in Flax Engine. I fully understand how all of the code works, and I directly copied 0 lines of code. I plan to release this code under the Zlib license. I’m hoping to get an opinion from someone who works at Epic.

(Note: It’s not 100% completed, but this gives an idea of what the script looks like)

float CharacterControllerPro::GetCurrentBrakingForce() const
{
    if (!_useSeperateBrakingForce)
    {
        return _brakingForce;
    }

    switch (_movementMode)
    {
        case MovementMode::Walking:
            return _walkingBrakingForce;
        case MovementMode::Falling:
            return _walkingBrakingForce;
        default:
            break;
    }

    return _brakingForce;
}

bool CharacterControllerPro::CanJump() const
{
    bool wasRecentlyOnGround = _coyoteTimer > 0;
    bool canJumpInAir = _airJumpCount < _airJumpsAllowed;
    bool canJump = _jumpingEnabled && (wasRecentlyOnGround || canJumpInAir);
    return canJump || _jumpingOverrideEnabled;
}

void CharacterControllerPro::CalculateVelocity()
{
    // Add any extra displacement we did to the last position
    Vector3 lastRelevantPosition = _lastPosition + _additionalDisplacement;
    // Calculate velocity based on our previous position
    _linearVelocity = (GetActor()->GetPosition() - lastRelevantPosition) / Time::GetDeltaTime();
    // Subtract the floor's velocity
    _linearVelocity -= _appliedFloorDelta / Time::GetDeltaTime();

    _lastPosition = GetActor()->GetPosition();
    _additionalDisplacement = Vector3::Zero;
}

void CharacterControllerPro::ApplyPlatformVelocity()
{
    // Only apply platform leave velocity if that setting is enabled
    if (_applyVelocityOnPlatformLeave && !_platformLeaveDelta.IsZero())
    {
        _characterController->Move(_platformLeaveDelta);
    }
}

void CharacterControllerPro::UpdateFloorInfo()
{
    _isFloorDetected = false;
    _floorInfo = RayCastHit();
    RayCastHit _hitResult;

    Vector3 sphereCenter = GetActor()->GetPosition();
    sphereCenter.Y -= (_characterController->GetHeight() / 2);
    sphereCenter.Y += _characterController->GetRadius() - 1;
    _isFloorDetected = Physics::SphereCast(sphereCenter, _characterController->GetRadius(), -_characterController->GetUpDirection(), _hitResult, _characterController->GetRadius() + _characterController->GetStepOffset(), _groundLayer);

    if (_isFloorDetected)
    {
        //_isOnFloor = true;
        _floorInfo = _hitResult;
        return;
    }

    _tryingToDetachFromGorund = false;
}

void CharacterControllerPro::UpdateFloorAnchor()
{
    if (!_stickToMovingPlatforms)
    {
        _floorAnchor = nullptr;
        _appliedFloorDelta = Vector3::Zero;
        return;
    }

    _platformLeaveDelta = _appliedFloorDelta;

    if (!_isOnFloor)
    {
        _floorAnchor = nullptr;
        _appliedFloorDelta = Vector3::Zero;

        return;
    }

    Actor* oldFloorAnchor = _floorAnchor;

    if (_floorInfo.Collider != nullptr)
    {
        _floorAnchor = _floorInfo.Collider;
    }

    // If we're now on a new surface, reset our floor transform cache
    if (!oldFloorAnchor || _floorInfo.Collider != oldFloorAnchor)
    {
        _floorAnchorLastPosition = _floorAnchor->GetPosition();
        _floorAnchorLastOrientation = _floorAnchor->GetOrientation();
    }

    Transform currentFloorTransform = Transform(_floorAnchor->GetPosition(), _floorAnchor->GetOrientation());
    Transform lastFloorTransform = Transform(_floorAnchorLastPosition, _floorAnchorLastOrientation);

    bool floorOrientationChanged = !Quaternion::NearEqual(_floorAnchorLastOrientation, _floorAnchor->GetOrientation());

    _floorAnchorLastPosition = _floorAnchor->GetPosition();
    _floorAnchorLastOrientation = _floorAnchor->GetOrientation();

    if (floorOrientationChanged && _rotateWithMovingPlatforms)
    {
        // Get the difference between the floor's current orientation and last orientation
        Vector3 deltaEuler = (currentFloorTransform.Orientation * Quaternion::Invert(lastFloorTransform.Orientation)).GetEuler();
        // Prevent the floor from causing us to rotate using pitch or roll
        deltaEuler.X = 0;
        deltaEuler.Z = 0;
        // Apply the rotation
        GetActor()->SetOrientation(GetActor()->GetOrientation() * Quaternion::Euler(deltaEuler));

        // Update our look rotation to reflect the rotation of the floor
        Vector3 newLookEuler = GetLookOrientation().GetEuler() + deltaEuler;
        SetLookOrientation(Quaternion::Euler(newLookEuler));
    }

    // Get our current world position relative to the floor's old transform
    Vector3 oldFloorPos = lastFloorTransform.WorldToLocal(GetActor()->GetPosition());
    // Determine what our world position would be if we were on the same spot relative to the floor's current transform
    Vector3 newFloorPos = currentFloorTransform.LocalToWorld(oldFloorPos);
    // Find out how much we need to move to get o that new relative position
    Vector3 floorAnchorPositionDelta = (newFloorPos - GetActor()->GetPosition());

    Vector3 previousPos = GetActor()->GetPosition();
    _characterController->Move(floorAnchorPositionDelta);

    // Our linear velocity shouldn't take into account any velocity from floor
    _appliedFloorDelta = (GetActor()->GetPosition() - previousPos);
}

void CharacterControllerPro::StopJump()
{
    _isJumping = false;
}

bool CharacterControllerPro::ShouldStopJump() const
{
    if (_maxJumpHoldTime == 0)
    {
        return true;
    }

    if (_jumpTime >= _maxJumpHoldTime)
    {
        return true;
    }

    return false;
}

void CharacterControllerPro::MoveJumping()
{
    // Only jump if the player recently pressed the jump button
    bool wantsToJump = _jumpBufferTimer > 0 || _justPressedJump;

    if (wantsToJump && CanJump() && !_isJumping)
    {
        // Reset our jump timers
        _jumpBufferTimer = 0;
        _jumpTime = 0;
        _isJumping = true;

        // If we were on the ground, apply platform velocity
        if (_coyoteTimer > 0 || _isOnFloor)
        {
            ApplyPlatformVelocity();
            _coyoteTimer = 0;
        }
        else
        {
            // We were in the air, so increment our air jump count
            _airJumpCount += 1;
        }
    }

    _justPressedJump = false;

    if (_isJumping)
    {
        // Prevent floor snappping from keeping us on the ground
        _tryingToDetachFromGorund = true;
        // Reset our falling velocity
        _linearVelocity.Y = 0;
        _movementMode = MovementMode::Falling;
        _isOnFloor = false;

        _jumpTime += Time::GetDeltaTime();

        // Apply jump force
        Vector3 jumpForce = Vector3(0, _jumpForce, 0);
        _characterController->Move(jumpForce * Time::GetDeltaTime());

        if (ShouldStopJump())
        {
            StopJump();
        }
    }

    _jumpBufferTimer = Math::Max(_jumpBufferTimer - Time::GetDeltaTime(), 0.0f);

    if (_isOnFloor)
    {
        _coyoteTimer = _coyoteTime;
        _airJumpCount = 0;
    }
    else
    {
        _coyoteTimer = Math::Max(_coyoteTimer - Time::GetDeltaTime(), 0.0f);
    }
}

Vector3 CharacterControllerPro::GetDesiredVelocity() const
{
    return Vector3::ClampLength(_movementInputVector * _maxSpeed, _maxSpeed);
}

Vector3 CharacterControllerPro::AccelerateToVelocity(const Vector3& currentVelocity, const Vector3& desiredVelocity, float maxAccel)
{
    // Find out how much acceleration we need to reach our desired velocity in a single fixed update
    Vector3 requiredAcceleration = (desiredVelocity - currentVelocity) / Time::GetDeltaTime();

    // Clamp it to the supplied max accel
    requiredAcceleration = Vector3::ClampLength(requiredAcceleration, maxAccel);

    return requiredAcceleration * Time::GetDeltaTime();
}

Vector3 CharacterControllerPro::CalculateFrictionVector(const Vector3& velocity, float friction, const Vector3& desiredMoveDirection)
{
    Vector3 frictionAmount = velocity;

    // Calculate friction direction from desired movement direction
    if (!desiredMoveDirection.IsZero())
    {
        // If a desired direction is supplied, use it as our friction amount
        Vector3 frictionDirection = desiredMoveDirection.GetNormalized() * velocity.Length();
        frictionAmount = (velocity - frictionDirection);
    }

    // Limit friction amount so it doesn't move us backward
    float frictionMultiplier = Math::Clamp(Time::GetDeltaTime() * friction, 0.f, 1.f);
    // Subtract our new friction vector from velocity
    return velocity - (frictionAmount * frictionMultiplier);
}

Vector3 CharacterControllerPro::CalculateNewMovement(const Vector3& currentVelocity, const Vector3& desiredVelocity, float friction, float maxBrakingForce)
{
    // Brake only if the character wants to stop or if we're moving faster than our MaxSpeed
    bool shouldBrake = _movementInputVector.IsZero() || currentVelocity.Length() > _maxSpeed;
    Vector3 desiredDirection = shouldBrake ? Vector3::Zero : desiredVelocity.GetNormalized();
    float accel = shouldBrake ? maxBrakingForce : _maxAcceleration;

    // Apply friction
    Vector3 newVelocity = CalculateFrictionVector(currentVelocity, friction * _frictionMultiplier, desiredDirection);

    // Apply it to our new velocity
    newVelocity += AccelerateToVelocity(currentVelocity, desiredVelocity, accel);

    if (!shouldBrake)
    {
        return newVelocity;
    }

    // Don't allow deceleration to lower our speed below 0
    if (Vector3::Dot(newVelocity, currentVelocity) < 0)
    {
        newVelocity = Vector3::Zero;
    }
    // If we started above max speed, don't allow deceleration to lower us below max speed
    else if (currentVelocity.Length() > _maxSpeed && Vector3::Dot(currentVelocity, desiredDirection) > 0 && newVelocity.Length() < _maxSpeed)
    {
        newVelocity = currentVelocity.GetNormalized() * _maxSpeed;
    }

    return newVelocity;
}

void CharacterControllerPro::HandleWalkOffFloor()
{
    _movementMode = MovementMode::Falling;
    ApplyPlatformVelocity();

    if (_tryingToDetachFromGorund)
    {
        return;
    }

    // Don't include the floor snapping in our velocity
    Vector3 floorSnapDisplacement = _lastFloorSnapDisplacement;
    _additionalDisplacement += floorSnapDisplacement;
}

void CharacterControllerPro::MoveWalking()
{
    // Calculate movement velocity
    Vector3 movementDelta = CalculateNewMovement(_linearVelocity, GetDesiredVelocity(), _walkingFriction, GetCurrentBrakingForce());
    // Turn our new velocity vector into a displacement vector
    movementDelta *= Time::GetDeltaTime();

    // Apply our movement
    _characterController->Move(movementDelta);

    _isOnFloor = _isFloorDetected;
    if (!_isOnFloor)
    {
        HandleWalkOffFloor();
        return;
    }

    // Snap character to floor
    if (!_tryingToDetachFromGorund)
    {
        Vector3 floorSnapDisplacement = Vector3::Up * -250.f * Time::GetDeltaTime();
        Vector3 oldPosition = GetActor()->GetPosition();
        _characterController->Move(floorSnapDisplacement);
        _lastFloorSnapDisplacement = GetActor()->GetPosition() - oldPosition;
    }
}

void CharacterControllerPro::MoveFalling()
{
    Vector3 lateralVelocity = _linearVelocity;
    lateralVelocity.Y = 0;

    // Calculate lateral movement velocity
    Vector3 movementDelta = CalculateNewMovement(lateralVelocity, GetDesiredVelocity(), _fallingFriction, GetBrakingForce());

    // Turn our new velocity vector into a displacement vector
    movementDelta *= Time::GetDeltaTime();

    //movementDelta += new Vector3(0, -50 * Time.DeltaTime, 0);

    _characterController->Move(movementDelta);

    Vector3 gravityVelocity = Vector3::Zero;
    gravityVelocity.Y = GetVelocity().Y;

    if (!(_isJumping && _disableGravityWhileHoldingJump))
    {
        // Sync our gravity with the physics system's gravity
        Vector3 gravityAmount = Physics::GetGravity() * _gravityScale;

        bool shouldUseFalloff = _useJumpGravityFalloff && GetVelocity().Y > _gravityFalloff;
        if (shouldUseFalloff)
        {
            gravityAmount *= _gravityFalloffMultiplier;
        }

        gravityVelocity.Y += gravityAmount.Y * Time::GetDeltaTime();
    }

    // Apply gravity
    _characterController->Move(gravityVelocity * Time::GetDeltaTime());
    _isOnFloor = _characterController->IsGrounded();

    if (_isOnFloor)
    {
        _movementMode = MovementMode::Walking;
    }
}

void CharacterControllerPro::RotateCharacter()
{
    if (_rotateTowards == RotationMode::NoRotation)
    {
        return;
    }

    float desiredYaw;

    switch (_rotateTowards)
    {
    case RotationMode::RotateTowardsDesiredVelocity:
        if (_lastMovementInputVector == Vector3::Zero)
        {
            return;
        }

        Vector3 lateralVelocity = Vector3(_lastMovementInputVector.X, 0, _lastMovementInputVector.Z);
        desiredYaw = Quaternion::LookRotation(lateralVelocity.GetNormalized(), Vector3::Up).GetEuler().Y;
        break;
    case RotationMode::RotateTowardsLookOrientation:
        desiredYaw = GetLookOrientation().GetEuler().Y;
        break;
    default:
        return;
    }

    if (_characterRotationVelocity == 0)
    {
        GetActor()->SetOrientation(Quaternion::Euler(0, desiredYaw, 0));
        return;
    }

    float newYaw = Math::MoveTowardsAngle(GetActor()->GetOrientation().GetEuler().Y, desiredYaw, _characterRotationVelocity * Time::GetDeltaTime());
    GetActor()->SetOrientation(Quaternion::Euler(GetActor()->GetOrientation().GetEuler().X, newYaw, GetActor()->GetOrientation().GetEuler().Z));
}

bool CharacterControllerPro::CheckIfActorDataIsValid(bool logMessage) const
{
    if (_characterController == nullptr)
    {
        if (GetActor() != nullptr)
        {
            if (logMessage)
            {
                auto errorMsg = TEXT("ChacterControllerPro script must be attached to a CharacterController actor, instead attached to: {}. (Of type {})");
                DebugLog::LogError(String::Format(errorMsg, GetActor()->GetName(), GetActor()->GetType().ToString()));
            }

            return false;
        }

        if (logMessage)
        {
            DebugLog::LogError(TEXT("CharacterControllerPro is unable to find a valid CharacterController."));
        }

        return false;
    }

    return true;
}

void CharacterControllerPro::OnAwake()
{
    
}

void CharacterControllerPro::OnStart()
{
    // Here you can add code that needs to be called when script is enabled (eg. register for events)
}

void CharacterControllerPro::OnEnable()
{
    Actor* actor = GetActor();
    _characterController = Cast<CharacterController>(actor);

    CheckIfActorDataIsValid(true);
}

void CharacterControllerPro::OnDisable()
{
    _characterController = nullptr;
    _customMovementFunction.Unbind();

    // Reset velocity
    _movementInputVector = Vector3::Zero;
    _lastMovementInputVector = Vector3::Zero;
    _lastPosition = GetActor()->GetPosition();
    _linearVelocity = Vector3::Zero;
    _additionalDisplacement = Vector3::Zero;
    _appliedFloorDelta = Vector3::Zero;
    _platformLeaveDelta = Vector3::Zero;

    // Reset jump
    _isJumping = false;
    _jumpTime = 0;
    _coyoteTimer = 0;
    _jumpBufferTimer = 0;
    _justPressedJump = false;
    _airJumpCount = 0;

    _floorAnchor = nullptr;
}

void CharacterControllerPro::OnFixedUpdate()
{
    if (!CheckIfActorDataIsValid())
    {
        return;
    }

    MoveJumping();

    UpdateFloorInfo();
    UpdateFloorAnchor();
    
    // Calculate movement based on our movement mode
    switch (_movementMode)
    {
        case MovementMode::Stopped:
            break;
        case MovementMode::Walking:
            MoveWalking();
            break;
        case MovementMode::Falling:
            MoveFalling();
            break;
        case MovementMode::Custom:
            if (_customMovementMode == nullptr)
            {
                DebugLog::LogWarning(TEXT("No custom movement mode specified in CharacterControllerPro."));
                break;
            }
            _customMovementMode->MoveCustom();
            break;
    }
    
    CalculateVelocity();
    
    // Reset inputs
    _lastMovementInputVector = _movementInputVector;
    _movementInputVector = Vector3::Zero;
    
    RotateCharacter();
}

Bump.

If there’s not a person who can give me an answer here, how can I contact Epic to ask this question?