实现效果

实现思路

锁定目标

用一个球形碰撞体来感知哪些敌人可锁定,碰撞体平时关闭,当玩家按下锁定键时,刷新碰撞体检测并开启一帧,在下一帧就知道哪些目标可锁定了,然后从这些可锁定的目标中找一个与摄像机正前方向量夹角最小的敌人作为锁定对象。
锁定目标后,让玩家视角固定,自身旋转实时朝向追踪敌人方向。

切换锁定目标

同样的,当玩家按下左或右键时,检测碰撞体开启一帧,然后在第二帧从除了当前锁定目标外所有可锁定敌人中,获取在玩家视角左/右边获取夹角最接近的敌人。夹角可以通过两向量点乘获取,然后通过公式:sign = (v1n.X - v1n.Y) * (v2n.Z - v2n.Y) - (v1n.Z - v1n.Y) * (v2n.X - v2n.Y),根据sign的正负得出是在左边还是右边。

实现步骤

因为我习惯用Lua所以代码部分使用的是Lua,如果你也想使用可以参照这篇文章:

首先在玩家身上挂一个碰撞体用于检测可锁定目标:

image.png
然后给也敌人挂上碰撞体,碰撞体的ObjectType为Enemy,玩家的检测碰撞体预设为只与Enemy发生Overlap:
image.png
image.png
最好是做成CollisionPresets,这里我就简单的手动调下。

核心代码

 
当按下锁定键时调用:

-- 尝试将视角锁定一个敌人
function cls:CheckLockEnemy_()
    self.enlockEnemyList_ = {}
    self:RefreshCheckEnemyCol_()  --开启一帧碰撞检测
    self:DelayCall(0.01, function()
        local cmpFunc = function(cAngle, angle)
            if angle == nil or math.abs(cAngle) < math.abs(angle) then
                return true
            end
            return false
        end
        local lockEnemy = self:GetLockEnemyByCmp_(cmpFunc)
        if lockEnemy ~= nil then
            self:LockEnemy_(lockEnemy)
        end
    end)
end

当按下切换左/右目标键时,调用:

-- 切换锁定目标
    if (key == GE.InputKey.Right or key == GE.InputKey.Left) and self.viewType_bp == CharacterEnums.ViewType.LockEnemy then
        self:RefreshCheckEnemyCol_()
        local cmpFunc = nil
        if key == GE.InputKey.Right then
            cmpFunc = function(cAngle, angle, cActor)
                if angle == nil then
                    if cActor ~= self.lockEnemy_ and cAngle < 0 then
                        return true
                    else
                        return false
                    end
                end
                if cAngle < 0 and cAngle > angle and cActor ~= self.lockEnemy_ then
                    return true
                end
                return false
            end
        else
            cmpFunc = function(cAngle, angle, cActor)
                if angle == nil then
                    if cActor ~= self.lockEnemy_  and cAngle > 0 then
                        return true
                    else
                        return false
                    end
                end
                if cAngle > 0 and cAngle < angle and cActor ~= self.lockEnemy_ then
                    return true
                end
                return false
            end
        end
        self:DelayCall(0.01, function()
            local lockEnemy = self:GetLockEnemyByCmp_(cmpFunc)
            if lockEnemy ~= nil then
                self:LockEnemy_(lockEnemy)
            end
        end)
    end

在Tick中:

function cls:ReceiveTick(deltaTime)
    self:BP_Tick(deltaTime)
    -- 如果锁视角,就根据目标手动刷新角色旋转
    if self.viewType_bp == CharacterEnums.ViewType.LockEnemy then
        local selfLoc, enemyLoc = GF:GetActorLocation(self), GF:GetActorLocation(self.lockEnemy_)
        if GF:IsActorValid(self.lockEnemy_) == false or self.lockEnemy_:IsDeath() or 
                GF:VSize(selfLoc - enemyLoc) > self.lockEnemyDistance_bp then
            self:UnLockEnemy_()
        else
            self:UpdateRotationByTarget_(self.lockEnemy_)
        end
    end
end

供调用的功能函数

-- 刷新检测碰撞体
function cls:RefreshCheckEnemyCol_()
    self["bp_EnemyCheckCol"]:SetCollisionEnabled(GE.CollisionEnabled.QueryOnly)
    local radius = self["bp_EnemyCheckCol"]:GetScaledSphereRadius()
    self["bp_EnemyCheckCol"]:SetSphereRadius(0, true)
    self["bp_EnemyCheckCol"]:SetSphereRadius(radius, true)
    self["bp_EnemyCheckCol"]:SetCollisionEnabled(GE.CollisionEnabled.NoCollision)
end
-- 锁定目标
function cls:LockEnemy_(lockEnemy)
    self.viewType_bp = CharacterEnums.ViewType.LockEnemy
    if self.lockEnemy_ then
        GD:Post(self, self.lockEnemy_, GEVT.LOCK_ENEMY, false)
    end
    GD:Post(self, lockEnemy, GEVT.LOCK_ENEMY, true)
    self.lockEnemy_ = lockEnemy
    self["bp_SpringArm"].bUsePawnControlRotation = false
    self.movementComp_.bOrientRotationToMovement = false
end
-- 解锁
function cls:UnLockEnemy_()
    self.viewType_bp = CharacterEnums.ViewType.Follow
    GD:Post(self, self.lockEnemy_, GEVT.LOCK_ENEMY, false)
    self.lockEnemy_ = nil
    self["bp_SpringArm"].bUsePawnControlRotation = true
    self.movementComp_.bOrientRotationToMovement = true
end

当发生碰撞时,将目标装入表:

self["bp_EnemyCheckCol"].OnComponentBeginOverlap:Add(function(overlappedComponent, otherActor, otherComp, otherBodyIndex, bFromSweep, sweepResult)
        self.enlockEnemyList_[otherActor] = otherActor
    end)

夹角计算:
(GF库:)

-- 计算目标对象与自身的夹角
function cls:GetTargetActorAngle(selfActor, targetActor)
    local v1 = self:GetForwardVector(self:GetActorRotation(selfActor))
    local tLoc, sLoc = self:GetActorLocation(targetActor), self:GetActorLocation(selfActor)
    tLoc.Z, sLoc.Z = 0, 0
    local v2 = tLoc - sLoc
    v1, v2 = self:Normal(v1), self:Normal(v2)
    local rad = math.acos(self:Dot_VectorVector(v1, v2))
    local angle = KismetMathLibrary:RadiansToDegrees(rad)
    return angle
end

-- 计算目标对象与自身的夹角(带正负)
function cls:GetTargetActorAngleHaveSign(selfActor, targetActor)
    local v1 = self:GetForwardVector(self:GetActorRotation(selfActor))
    local tLoc, sLoc = self:GetActorLocation(targetActor), self:GetActorLocation(selfActor)
    tLoc.Z, sLoc.Z = 0, 0
    local v2 = tLoc - sLoc
    local v1n, v2n = self:Normal(v1), self:Normal(v2)
    local rad = math.acos(self:Dot_VectorVector(v1n, v2n))
    local angle = KismetMathLibrary:RadiansToDegrees(rad)
    local sign = (v1n.X - v1n.Y) * (v2n.Z - v2n.Y) - (v1n.Z - v1n.Y) * (v2n.X - v2n.Y)
    -- print("hcDel sign angle = ", angle, v1n.X, v1n.Y, v1n.Z, v2n.X, v2n.Y, v2n.Z)
    if sign < 0 then
        angle = -1 * angle
    end

    return angle
end
-- 看向目标旋转
function cls:UpdateRotationByTarget_(target)
    local curRot = GF:GetActorRotation(self)
    local tarRot = GF:FindLookAtRotation(GF:GetActorLocation(self), GF:GetActorLocation(target))
    curRot.Roll, curRot.Pitch = 0, 0
    tarRot.Roll, tarRot.Pitch = 0, 0
    local newRot = GF:RInterpTo(curRot, tarRot, GF:GetDeltaTime(), 5)
    GF:SetActorRotation(self, newRot, false)
end

最后修改:2022 年 09 月 07 日
如果觉得我的文章对你有用,请随意赞赏