4

黑魂复刻游戏的玩家控制器(锁定状态(1))——Unity随手记

 2 years ago
source link: https://segmentfault.com/a/1190000040141710
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

黑魂复刻游戏的玩家控制器(锁定状态(1))——Unity随手记

发布于 28 分钟前

今天实现的内容:
新增锁定输入
为输入模块添加锁定键和锁定信号,更新信号。

    --- IPlayerInput
    public bool lockOn; //锁定信号
    --- JoystickInput
    public MyButton buttonLockOn = new MyButton(); //锁定键
    
    // Update is called once per frame
    void Update()
    {
        // 更新按键
        buttonLockOn.Tick(Input.GetButton(btnRS));
        // 锁定信号
        lockOn = buttonLockOn.onPressed;

锁定和解锁的代码逻辑和摄像机代码逻辑
首先,要锁定目标,先要确定要锁定的目标是什么。我们使用Physics.OverlapBox来得到指定盒子区域内的碰撞体。
image.png
锁定需要始终将摄像机对准目标。所以我们在CameraController中添加新方法LockOn_or_Unlock用来处理锁定解锁逻辑,然后在PlayerController中调用该方法。

    // 摄像机锁定/解除锁定
    public void LockOn_or_Unlock()
    {
        // 尝试去锁定一个
        Vector3 tmp_modelCenter = modelGO.transform.position + Vector3.up; //获得模型的中心
        Vector3 tmp_boxCenter = tmp_modelCenter + modelGO.transform.forward * 5.0f; //得到OverlapBox的中心
        Collider[] cols = Physics.OverlapBox(tmp_boxCenter,  //通过OverlapBox尝试获取范围内Enemy标签的碰撞体
            new Vector3(1.0f, 1.0f, 5.0f),
            modelGO.transform.rotation,
            LayerMask.GetMask("Enemy"));     
        if (cols.Length == 0)
        {
            // 如果没有检测到任何东西 将lockTarget设置为null
            lockTarget = null;
        }
        else
        {
            //如果得到碰撞体 将第一个赋值给lockTarget
            lockTarget = cols[0].gameObject;
        }
    }

LockOn_or_Unlock会使用OverlapBox查看给定范围内是否有碰撞体,如果找到了碰撞体,将数组中的第一个给lockTarget,如果没找到则设置为null。
如果摄像机没有锁定,则在FixedUpdate中将playerController按输入设置旋转。如果锁定了,则摄像机要计算模型和锁定目标的方向向量,把这个方向向量交给摄像机,在我们的架构中,直接用来设置PlayerController的forward就行,记得要将该向量的y轴设置为0。

    void FixedUpdate()
    {
        if (lockTarget == null) //如果没有锁定目标 按输入控制PlayerController旋转
        {
            // 得到摄像机旋转前的模型欧拉角
            Vector3 temp_modelEuler = modelGO.transform.eulerAngles;
            // 左右旋转时直接旋转PlayerHandle 摄像机也会跟着
            playerController.transform.Rotate(Vector3.up, current_pi.cameraRight * horizontalSensitivity * Time.fixedDeltaTime);
            // 摄像机旋转后将模型原来的欧拉角再赋给模型 保证模型不动
            modelGO.transform.eulerAngles = temp_modelEuler;
        }
        else //如果锁定了目标 计算从模型到锁定目标的方向向量 将PlayerController的forward设置为该向量
        {
            // 计算方向向量
            Vector3 temp_forward = lockTarget.transform.position - modelGO.transform.position;
            // 将向量的y轴设置为0 PlayerController的Y轴不需要旋转
            temp_forward.y = 0;
            // 设置PlayerController的forward
            playerController.transform.forward = temp_forward;
            // 设置LockonIcon的图片位置
            lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position);
        }

        // 上下旋转时旋转CameraHandle
        temp_eulerX -= current_pi.cameraUp * verticalSensitivity * Time.fixedDeltaTime;
        // 限制俯仰角
        temp_eulerX = Mathf.Clamp(temp_eulerX, -40, 30);
        // 赋值localEulerAngles
        cameraHandle.transform.localEulerAngles = new Vector3(temp_eulerX, 0, 0);

        // 摄像机的位置通过SmoothDamp来实现一种延迟移动的效果
        cameraGO.transform.position = Vector3.SmoothDamp(
                cameraGO.transform.position, this.transform.position, ref temp_dampValue, 0.1f * current_pi.dirMag);
        // 让摄像机保持看向一个位置 防止位置进行SmoothDamp时的抖动
        cameraGO.transform.LookAt(cameraHandle.transform);
    }

锁定的提示UI
为了在游戏中提示玩家当前锁定了哪个目标,我们要加入一个UI。
image.png
需要在没有进行锁定时将其enabled设置为false。只在锁定时将其设置为true。最后将UI的位置放到锁定的对象身上。

    // 设置LockonIcon的图片位置
    lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position);

锁定的控制器代码逻辑
锁定时控制器中的代码和摄像机类似,一个是设置摄像机对准目标,一个是设置模型对准目标。只有当模型对准目标,我们才能引入锁定时相应的动画。同样重要的还有锁定状态下的移动计算,由于锁定时模型和PlayerController的方向都已锁死,此时方向的判断不根据模型了,而是直接来自输入的产生方向,我们在输入模块中用dirVec来表示。

    // -- PlayerController --

    // Update is called once per frame
    void Update()
    {
        // ...
        // 触发锁定 
        if (current_pi.lockOn)
        {
            camCon.LockOn_or_Unlock();
        }
        if (camCon.isLockon) //摄像机已锁定 将模型旋转设定为朝向目标
        {
            // 由于已经在CameraController设置了PlayerController对象的旋转所以直接给模型就行
            model.transform.forward = this.transform.forward;
            // 计算锁定时的移动
            if (!lockPlanar)
            {
                m_planarVec = current_pi.dirVec  * walkSpeed * (current_pi.run ? runMultiplier : 1.0f);
            }
        }
        else //摄像机没有锁定 根据输入控制模型旋转
        {
            // 只在有速度时能够旋转 防止原地旋转
            if (current_pi.dirMag > 0.1f)
            {
                // 运用旋转 使用Slerp进行效果优化   
                model.transform.forward = Vector3.Slerp(model.transform.forward, current_pi.dirVec, 0.3f);
            }
            // 计算没有锁定时的移动量
            if (!lockPlanar)
            {
                m_planarVec = current_pi.dirMag * model.transform.forward * walkSpeed * (current_pi.run ? runMultiplier : 1.0f);
            }
        }
    }

总结一下,由于我们水平旋转摄像机是靠旋转PlayerController游戏对象来实现,所以锁定目标将PlayerController游戏对象的forward指向目标就行。同样的,由于已经设置了PlayerController在锁定时指向目标,要将模型指向目标只需要将PlayerController对象的forward给模型就行。移动的方向判断需要通过输入产生的方向,也就是我们之前做输入模块时写的dirVec来判断。

锁定的动画

之前说要将模型始终对准目标才能引入锁定对应的动画,锁定对应的动画就是始终朝向一个方向时的前进后退和左右侧步。要加入这些动画,我们需要将ground混合树修改为2D Freeform类型。原来的移动依然只由forward参数操控,我们要额外加入right参数,在锁定时,我们将通过修改right参数操控左右侧步,以及调整forward为负值来操控后退。这样还不需要加入新混合树就可以实现原来的移动和锁定时的移动了。
image.png
至于动画参数的代码,结构和之前的逻辑类似,在锁定状态下要调整的是forward和right,非锁定状态下只调整forward就行。具体代码如下:

    // Update is called once per frame
    void Update()
    {
        // --------------------- 动画参数 ---------------------
        if (camCon.isLockon)
        {
            Vector3 localDirVec = this.transform.InverseTransformDirection(current_pi.dirVec);
            anim.SetFloat("forward", localDirVec.z * ((current_pi.run) ? 2.0f : 1.0f));
            anim.SetFloat("right", localDirVec.x * ((current_pi.run) ? 2.0f : 1.0f));
        }
        else
        {
            anim.SetFloat("forward", current_pi.dirMag * Mathf.Lerp(anim.GetFloat("forward"), (current_pi.run) ? 2.0f : 1.0f, 0.5f));
            anim.SetFloat("right", 0);
        }
        
    // ...

    }

锁定状态下的翻滚和跳跃
由于锁定状态下模型方向判断和之前不一样了,所以我们需要重新设计锁定状态下的翻滚和后跳。设计方案是,设置一个新的bool值trackDirection,当该bool为true时,将追踪m_planarVec,提供给翻滚后跳作为方向。当我们开始跳跃或翻滚时,将trackDirection设置为true。落地时设置为false。

    // 进入Base层的动画节点roll时执行的方法
    // 通过PlayerController动画机中的roll节点上挂载的FSMOnEnter调用
    public void OnRollEnter()
    {
        // 关闭输入模块
        current_pi.inputEnabled = false;
        // 锁定平台移动计算
        lockPlanar = true;
        // 运用翻滚冲量
        m_planarVec = m_planarVec.normalized * rollThrust;
        // 追踪dirVec
        trackDirection = true;
    }

当锁定时,要旋转模型的时候,如果trackDirection为true,则按照m_planarVec作为方向。

        // --------------------- 模型旋转和位移 ---------------------
        if (camCon.isLockon) //摄像机已锁定 将模型旋转设定为朝向目标
        {
            if(trackDirection) //当前是否追踪方向
            {
                model.transform.forward = m_planarVec.normalized;
            }
            else
            {
                // 由于已经在CameraController设置了PlayerController对象的旋转所以直接给模型就行
                model.transform.forward = this.transform.forward;
            }

最后,将动画机调整一下,将right参数考虑进翻滚和跳跃。
image.png
到目前为止,我们的锁定功能基本刊用了。

BUG以及缺陷:
还是老问题,我们使用的动画在放入动画机混合树之后产生了一些不协调,还是需要美术大大们帮忙才行。如果硬是要解决,下图展示了老师提供的方案。聊胜于无。
image.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK