随手记3:C#Unity中随机数的使用
Posted 高贵的小汤
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了随手记3:C#Unity中随机数的使用相关的知识,希望对你有一定的参考价值。
问题:在同时引用UnityEngine和System命名空间时【using UnityEngine(继承MonoBehaviour类+)+using System(用到了浮点数转字符串的Convert函数)】,如果直接Random.Range(float1float2)会出现“Random”是“UnityEngine.Random”和“System.Random”之间的不明确的引用的报错
解决方法:在前面加上到底使用哪个Random,即UnityEngine.Random.Range(float1float2)或者先System.Random r = new System.Random(); 再调用r.Next(int1,int2);
注:System.Random需要实例化才能随机,而UnityEngine.Random是直接使用。
Random.Range以及Random.value,前者是方法,后者是变量。
补充:除了需要随机数之外,可能会有保留上次随机结果的需求,换句话说,从某一时刻起,我们希望每次都能随机出和上次相同的结果,这个时候就该随机种子出场了。
随机种子就是一个种子对应着一个结果,随机种子对应的就是一个唯一的随机结果。
- 在使用UnityEngine库时
seed = PlayerPrefs.GetInt("Seed");
//修改种子数,用到System.DateTime.Now.Ticks来保证得到和上次的种子绝不相同的整型
PlayerPrefs.SetInt("Seed", (int)System.DateTime.Now.Ticks);
//每次随机生成不同的序列
Random.Range(int1,int2 );
用法:在某次发现了随机产生的其他Bug,这样我只要我只用启动Debug模式反复分析几遍后把复现的隐藏Bug修改结束后再回到正常模式产生新的随机数就好。
(以是否开启启动Debug模式作为判断,若开启则seed = PlayerPrefs.GetInt("Seed")数据不变,关闭Debug模式时PlayerPrefs.SetInt("Seed", (int)System.DateTime.Now.Ticks)数据改变)
- 在使用UnityEngine库时
一个构造方法带有随机种子的参数,一个则没有,原理和上面是一样的:
public Random();
public Random(int Seed);
黑魂复刻游戏的玩家控制器(锁定状态)——Unity随手记
今天实现的内容:
新增锁定输入
为输入模块添加锁定键和锁定信号,更新信号。
--- 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来得到指定盒子区域内的碰撞体。
锁定需要始终将摄像机对准目标。所以我们在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 lockTarget = cols[0].gameObject; lockonIcon.enabled = true; // 设置LockonIcon的图片位置 lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position); isLockon = true; } else { // 如果没有检测到任何东西或者已经锁定了目标 将lockTarget设置为null lockTarget = null; lockonIcon.enabled = false; isLockon = false; } }
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; // 上下旋转时旋转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); } 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; // 锁定摄像机看目标的“脚底” cameraHandle.transform.LookAt(lockTarget.transform.parent.position); } // 摄像机的位置通过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。
需要在没有进行锁定时将其enabled设置为false。只在锁定时将其设置为true。最后将UI的位置放到锁定的对象身上。
private void Update() { if(isLockon) { // 更新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为负值来操控后退。这样还不需要加入新混合树就可以实现原来的移动和锁定时的移动了。
至于动画参数的代码,结构和之前的逻辑类似,在锁定状态下要调整的是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参数考虑进翻滚和跳跃。
自动解除锁定
到目前为止,我们的锁定功能基本刊用了,但是每当游戏玩家想要解除锁定时必须再次按下锁定键,否则无论跑出多远都会锁定到目标上,从游戏设计的角度来讲,玩家跑出很远的距离一般都是希望取消锁定的,而且如果在很远距离都能锁定敌人会导致一些追踪魔法能打很远。所以最后我们再做一个自动解除锁定功能。
通过计算玩家和目标的距离,如果距离大于某个值,则自动执行解锁。
private void Update() { // 如果锁定了目标 if(isLockon) { // 更新LockonIcon的图片位置 lockonIcon.rectTransform.position = Camera.main.WorldToScreenPoint(lockTarget.transform.position); // 超出某个距离自动解除锁定 if (Vector3.Distance(lockTarget.transform.position, modelGO.transform.position) > 10f) { lockTarget = null; lockonIcon.enabled = false; isLockon = false; } } }
以上是关于随手记3:C#Unity中随机数的使用的主要内容,如果未能解决你的问题,请参考以下文章
黑魂复刻游戏的玩家控制器(后跳奔跑翻滚跳跃的再重新设计)——Unity随手记