本篇博客为游戏开发记录,博主只是想自己做个移动组件给自己做游戏用,此间产生的一些经验也做一个分享。

简介

为了在3D世界中自由的控制我们的角色,引擎一般会提供一些基础的移动组件,上层用户做提供一些每帧的速度输入,移动组件应该返还一个正确的位置,一般来说就是保证不会穿模和沿着墙面滑行。

为了达成这个目的 常规思路有两种,一种是直接使用动力学 rigidbody,另一种是基于运动学rigidbody,或者你的世界完全没有物理交互 那么也可以无rigidbody。

动力学刚体组件

第一种动力学rigidbody,相信大家都不陌生,就是让物理引擎去接管我们的运动,我们提供力 速度写入,物理引擎会自动判断周围有哪些物体,然后使用碰撞检测、碰撞处理算法去解动力学求交,并且各种速度、加速度、角速度都会因为动力学求解而非常的自然。但是对于很多游戏而言想要驾驭动力学其实也是一件难事,比如我们的主角 通常都要保持一个直立的站立、稳定的旋转量,于是一般这类基于动力学刚体的移动组件都会锁住旋转量,或者纯动力学利用joint或者其他机制来保证主角的平衡。

动力学刚体我找到了一个案例,来自CatCoding的 movement实现:https://catlikecoding.com/unity/tutorials/movement/

运动学刚体组件

第二种则是运动学刚体,Kinematic,这类刚体和前面的刚体的主要区别在于,运动学刚体,物理引擎是将其当成质量无限大的物体来处理的,这样各种碰撞交叉产生的挤出便不会对此类刚体生效,此类刚体可以实现想去哪就去哪,可以被上层Gameplay逻辑精准控制位置,同时PhysX也会对这类刚体的运动有所记录,运动学刚体可以挤开各种动力学刚体。

如果要基于此类刚体做移动组件,主要就是解决和其他碰撞体的交叉问题,让运动学刚体运动不穿模,动力学刚体的挤开则是完全根据质量等进行物理计算,运动学其实在这块把这类问题暴露给上层,理论上交给上层的控制性更高。

其实Unity有提供一个现成的组件,名字叫CharacterController,他其实是PhysX基于Capsule的一个移动封装,很多项目也都在使用。

网络上也有一些开源的KCC插件,比如说

OpenKCC

UnityAsset Store的Kinematic Character Controller

其实Unreal提供的移动组件也是一个KCC的实现,实现的功能很多,提供了状态机的思路,所以大家用起来才这么方便,可以参考一下知乎的移动组件剖析文章:《Exploring in UE4》移动组件详解[原理分析]

这一些移动组件我都大体有看过,我这边参考UnityAssetStore的框架结构结合一些UE的思想做了一个简单的移动组件。

移动组件设计

设计原则

这里先说一下移动组件的设计目的,解决什么问题。

上层只用输入一个速度值,速度值传入移动组件,移动组件根据速度计算当帧的位移量,然后根据此位移量做移动预测,在预测位置做位置校准,保证最后输出的位置是不穿模的位置。并且提供一些状态量给上层,包括有没有踩到地面,踩到的地面是不是一个斜面,当帧有没有发生碰撞的事件,什么时候落地的事件,以及帮助上层处理上台阶的问题。

至于你想实现某些地形要做什么特殊处理,都可以基于提供的状态量做拓展,比如在斜面上的行为完全在上层写,或者自己再做一些额外的场景查询功能,自己维护状态。

运行流程

以下是我FixedUpdate里会跑的运行流程。

void Simulate()
{
characterController.BeforeCharacterUpdate(Time.fixedDeltaTime);
TimeIntegration();
InitPositionOverlapTest();
GroundDetection();
MovementDetection();
PendingLeaveGroundLoop();
ApplayDeltaPos();
characterController.AfterCharacterUpdate(Time.fixedDeltaTime);
}

玩家首先在每帧Update进行Input输入,我们在update里把这些数据记录下来,传入移动组件,然后移动组件在FixedUpdate里对这些记录的输入状态进行速度改变,注意一定是FixedUpdate里做这些速度改变,因为unity涉及物理的tick都跑在FixedUpdate。因此我们移动组件可以提供一个UpdateVelocity接口,让上层所有的运动速度修改都走这个接口,这样就可以保证不会在错误的时机写入速度。

TimeIntegration 与 ApplayDeltaPos

整个运行流程一开始试一次时间积分,用于记录一下当帧的移动组件位置targetPos,跑UpdateVelocity的逻辑改变速度,改变后的速度进行积分得到造成的当帧位移量deltaPos,targetPos会在最后和deltaPos相加然后走Kinematic 刚体的MovePosition和MoveRotation接口来应用。

GroundDetection

地面检测要处理是否踩到地面,是否踩到斜面,对于很低的小碰撞体要可以跨过去(其实也就是支持上台阶)。

这里给出一句简要总结: 斜面是特殊的地面,台阶是特殊的斜面。

地面检测的流程:

1、从上往下做CapsuleCast 寻找潜在的地面碰撞体,取最近的

2、如果cast中了地面就根据预测位置进行Ovelap,检测是否和地面碰撞体相交

3、如果检测到了相交就ComputePenetration计算怎样解相交,将计算出的解相交的位移量进行应用

当我们真的完成了一次解相交 便可以做一个抛出一个落地了的事件。

这里画了一幅图 ,

黑色 原始位置 

绿色 预测位置 

蓝色 校正位置

红色是cast 用来确定是和哪个碰撞解穿插

黄色使我们一次计算得到实际位移量。

到底什么算地面

碰撞点在capsule的下半身的位置,这里可以定义一个最大的地面碰撞点高度,排除上半身cast到东西的情况。

需要注意的是Cast的起点应该向上提一些,这样避免胶囊体贴住地面、有一定交叉的时候Cast不到地面。

另外CapsuleCast得到的RaycastHit的Normal有一些波动,不是正确的Normal,需要进行Normal修正,特别是在斜面判断上。

斜面如何判断

根据cast出的地面的法线,计算夹角,给出一个最大的能上斜面的角度,夹角大于这个度数则判断为斜面,需要指出正对的属于90度的斜面。

我们每帧的即将产生的位移量应该在这个法向量所在的平面进行投影,这样就能实现沿着斜面运动。

台阶如何判断

首先碰撞点低于下半身球的碰撞点,已经过滤了一波,说明你面对的是一个很低的阻挡物,接下来我们通过一些额外的Raycast可以判断他是否是台阶。

我这里就是通过两层射线,下层射线和上层射线,下层射线比上层射线短一点,如果下层射线打中了上层射线没打到则说明这是个台阶,很简单粗暴,但是很有效。

然后台阶 = 角度很大的斜面,此时经过一次ComputePenetration,我们的状态会在不稳定斜面上,此时我们就可以判断,加入此时的输入的deltaPos是朝向着这个很大的斜面的。就可以判断能不能上台阶了,如果能上就执行上台阶的功能。

具体上台阶就是让deltaPos 向上方和原来的速度方向做一个调整,同时因为是上台阶,可以将当前速度的y方向改成0,等于你踩在了台阶上。

跳跃如何实现

我们在地面上运动 如果要起跳就是把y轴速度置为一个值,然后传进去,此时我们的地面检测应该先关闭一小段时间,并且把movement在地面上的状态置为false,来保证上层按下跳跃之后的下一帧就能起跳,避免下一帧还是movement在地面上状态为true,影响上层逻辑PendingLeaveGroundLoop就是做了简单定时器。

当需要跳跃的时候就RequestJump,这样定时器就会启动保证落地状态正确。

MovementDetection

移动检测 其实非常简单

包含两个部分

1、初始位置错误

比如我们提供了直接SetPosition这样的传送接口,传送之后的位置如果有和其他东西相交,那么先解这个相交,这个过程在InitPositionOverlapTest里做了。

2、当帧速度应用后位置错误

将targetPos+deltaPos得到当帧预测位置,对预测位置进行OverlapTest,然后按顺序解各个overlap的相交量。

当然我们也会过滤一些碰撞体,比如纯动力学物体我就不进行解相交,这样我们的运动组件就可以挤开一些动力学的物体,不过这样其实也不是很好,会遇到各种问题,比如把一些动力学的东西挤到墙里去,其实计算一下动力学物体的渗透深度然后改速度应用给这些动力学物体会是一个好的解法。

如果我们真的解掉了相交,那么就可以抛出一个碰到了东西的事件。

总结

好的,完成了以上步骤我们就基本能得到一个简单的,可以在地面上跑来跑去可以跨越台阶的可以跳以及移动不会穿模的简单移动组件了。

为什么要自己做一个移动组件呢,主要身为游戏开发者,还是我认为对于移动这种最基本的东西还是应该有绝对的把握。把很多的运动交给物理引擎是很方便,但是当我们觉得不满的时候想改起来在上层还是困难比较多,自己实现一个简单的再根据项目检验来完善自己的移动组件会是一件有价值的事情。

我同时也在B站上传一个视频,可以点击链接来看看效果。

想明白了,开始做了,做的有反馈,想的愈明白。

感谢你的阅读,我是飞翔的子明,期待我们都做出我们心中的游戏。

2023.6.30

Unity 制作KinematicCharacterController的更多相关文章

  1. Unity制作FPS Demo

    等到把这个Unity FPS Demo[僵尸杀手]完成后再详细补充一下,使用Unity制作FPS游戏的经历,今天做个标识.

  2. Unity制作游戏中的场景

    Unity制作游戏中的场景 1.2.3  场景 在Unity中,场景(Scene)就是游戏开发者制作游戏时,所使用的游戏场景.它是一个三维空间,对应的三维坐标轴分别是X轴.Y轴和Z轴本文选自Unity ...

  3. 使用Unity制作游戏关卡的教程(三)

    转自:http://gamerboom.com/archives/75593 作者:Matthias Zarzecki 本文是“使用Unity制作<The Fork Of Truth>的关 ...

  4. 使用Unity制作游戏关卡的教程(二)

    转自:http://gamerboom.com/archives/75554 作者:by Matthias Zarzecki 本文是“使用Unity制作<The Fork Of Truth> ...

  5. 使用Unity制作游戏关卡的教程(一)

    转自: http://gamerboom.com/archives/74131 作者:Matthias Zarzecki 我正在制作<Looking For Group – The Fork O ...

  6. 如何使用Unity制作虚拟导览(一)

    https://www.cnblogs.com/yangyisen/p/5108289.html Unity用来制作游戏已经是目前市场上的一个发展趋势,而且有越来越多的公司与开发者不断的加入,那么Un ...

  7. Unity制作地形的常用插件之GAIA

    用Unity制作大型游戏少了地形制作怎么行,用原生的Unity工具制作地形效率较低而且也不甚美观,后期运行的效率也得不到保证.下面推荐的专业地形制作工具可以帮助开发者解决地形制作的相关问题. 打开Un ...

  8. Unity制作王者荣耀商业级手游

    <王者荣耀>这种现象级手机游戏是如何制作出来的呢?本文以<王者荣耀>MOBO类型的多人在线战术竞技游戏为入口,覆盖Unity游戏制作开发前端与Node.js服务器端的开发必备知 ...

  9. 利用Unity制作“表”

    一枚小菜鸟   目前没发现在Unity有其他路径制作类似于c# WinForm中的表:但是利用Unity自带的UGUI,制作了一张"伪表",具体方案如下: 效果图如下: 步骤: 1 ...

  10. Unity 制作RPG地图2(自己控制地图上图标)

    上一次用Unity摄像机方式实现了地图的制作,现在介绍另一种实现地图的方式: 自己通过代码实现小地图NCP图标的显示和隐藏 制作地图的步骤: 1. 根据游戏人物的3D坐标转换成2D平面坐标,根据距离显 ...

随机推荐

  1. 四月七号java基础学习

    1.数据类型分为基本数据类型以及引用数据类型 基本数据类型有整型.浮点型.字符型.布尔型 引用数据类型有类.数组以及接口 2.常量的声明需要用关键字final来标识 3.JAVA语言的变量名称由数字, ...

  2. Java设计模式 —— 面向对象设计原则

    1 设计模式概述 1.1 设计模式的定义与分类 设计模式的定义 Design patterns are descriptions of communicating objects and classe ...

  3. LeeCode 433 最小基因变化

    LeeCode 433 最小基因变化 题目描述: 基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'.'C'.'G' 和 'T' 之一. 假设我们需要调查从基因序列 start ...

  4. 12年经验的大龄程序员,都用什么写 API 文档?

    写代码,程序员不害怕. 写文档,每个程序员都害怕! 为什么? 技术优先,我们更倾向于将技能和精力更多地放在编写代码上,如果 API 工具不好使,不便捷,同步麻烦,测试看不懂,更会大大地打击编写文档的积 ...

  5. [GAUSS-50201]:The /opt/software/openGauss/xxxx-RedHat-64bit.tar.bz2 does not exist

    问题描述:使用redhat7.9来安装opengauss集群,预安装过不去.opengauss官方只支持centos版本,最好是centos7.6. [root@db01 script]# ./gs_ ...

  6. Python程序笔记20230304

    抛硬币实验 random 模块 import random random.randint(a, b) 返回一个随机整数 N,范围是:a <= N <= b random.choice(&q ...

  7. 【MyBatis】分页插件

    分页插件 分页插件配置 a 添加依赖 <dependency> <groupId>com.github.pagehelper</groupId> <artif ...

  8. 【SpringMVC】(二)RESTFul

    RESTFul RestFul简介 REST:Representational State Transfer,表现层资源状态转移 资源:资源是一种看待服务器的方式 资源的表述:资源的表述是资源在某个特 ...

  9. jmeter参数化导致反斜杠(\)被转义

    前情提要:在用jmeter做接口测试时,对请求体进行参数化,执行结果报错.但在不参数化的情况下,执行结果成功,而且参数化后,请求中读取到的参数是正确的(执行失败与执行成功时的参数一致). 问题排查:参 ...

  10. .NET敏捷开发框架-RDIFramework.NET V5.1发布(跨平台)

    RDIFramework.NET,基于全新.NET Framework与.NET Core的快速信息化系统敏捷开发.整合框架,给用户和开发者最佳的.Net框架部署方案.为企业快速构建跨平台.企业级的应 ...