动画事件的碰撞器触发
Physics
类的常用方法
RaycastHit hit;
if (Physics.Raycast(origin, direction, out hit, maxDistance)) {
Debug.Log("Hit: " + hit.collider.name);
}
Physics.Raycast
:从指定点向某个方向发射射线,检测是否与碰撞体相交。
Physics.RaycastAll
:发射射线并返回所有碰撞到的对象
Physics.IgnoreCollision
:忽略两个碰撞体之间的碰撞。
Physics.IgnoreCollision
方法可以用来忽略两个碰撞体之间的碰撞,但它并没有直接提供查看已忽略碰撞的功能。
如果你需要跟踪哪些碰撞体之间的碰撞被忽略,可以考虑手动维护一个列表或字典,用于记录哪些碰撞体之间的碰撞被忽略。你可以在调用 Physics.IgnoreCollision
时,将被忽略的碰撞体记录到这个列表中。
Dictionary<Collider, List<Collider>> ignoredCollisions = new Dictionary<Collider, List<Collider>>();
void IgnoreCollision(Collider a, Collider b) {
Physics.IgnoreCollision(a, b);
if (!ignoredCollisions.ContainsKey(a)) {
ignoredCollisions[a] = new List<Collider>();
}
ignoredCollisions[a].Add(b);
}
额外的写法,搭配元组 绑定两个碰撞体之间的关系值
Dictionary<(Collider, Collider), bool> ignoreds = new Dictionary<(Collider, Collider), bool>();
void IgnoreCollision(Collider a, Collider b) {
Physics.IgnoreCollision(a, b);
ignoreds[(a, b)] = true; // 记录忽略的碰撞
}
void LogIgnoreds() {
foreach (var pair in ignoreds.Keys) {
Debug.Log($"Ignored Collision: {pair.Item1.name} - {pair.Item2.name}");
}
}
Dictionary<(Collider, Collider), bool> ignoredCollisions = new Dictionary<(Collider, Collider), bool>();
// 记录忽略的碰撞
ignoredCollisions[(colliderA, colliderB)] = true;
Physics.gravity
:获取或设置全局重力向量,影响所有物体的重力效果。
只对于场景上附加了 Rigidbody
组件的物体,Physics.gravity
会直接影响它们的重力。默认情况下,Rigidbody
会根据这个全局重力向量施加重力。
Physics.DrawRay
:用于在场景视图中可视化射线,便于调试。
帮助你可视化射线,确保其正确指向目标
public static void DrawRay(Vector3 start, Vector3 direction, Color color = Color.white, float duration = 0f);
start (Vector3
):射线起点
direction (Vector3
):射线的方向,通常是一个单位向量
ps:Physics.DrawRay
实际上并没有直接提供长度参数,它只需要起点和方向向量。
要使用射线的长度,你通常需要将方向向量再乘以要的长度,然后计算出终点。
color (Color
):射线的颜色,默认是白色。
Debug.DrawLine
可以绘制从 start
到 end
的线,两者是有些区别的
Debug.DrawRay(start, direction, Color.red);
Physics.OverlapSphere
Collider[] Physics.OverlapSphere(Vector3 position, float radius, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);
返回一个 Collider
数组,包含所有在指定位置和半径内的碰撞体。
position (Vector3
):球体中心的位置,表示检测的起始点。
radius (float
):球体的半径,决定了检测的范围。
layerMask (int
):可选参数,指定哪些层的碰撞体将被检测。默认为所有层。通过位掩码可以更精确地选择层。
queryTriggerInteraction (QueryTriggerInteraction
):可选参数,指定是否检测触发器(Trigger)。可以选择以下值:
QueryTriggerInteraction.UseGlobal
:使用全局设置。QueryTriggerInteraction.Ignore
:忽略触发器。QueryTriggerInteraction.Collide
:包括触发器。
void Update() {
// 定义球体中心和半径
Vector3 sphereCenter = transform.position;
float sphereRadius = 5f;
// 获取在范围内的碰撞体
Collider[] hitColliders = Physics.OverlapSphere(sphereCenter, sphereRadius);
// 遍历所有碰撞体
foreach (Collider collider in hitColliders) {
// 输出碰撞体的名称
Debug.Log("Detected: " + collider.name);
}
}
拾取物品、感知范围、区域触发事件
摄像机移动的四元数
只传入一个方向向量给 Quaternion.LookRotation
时,它会默认使用世界坐标系的 Y 轴(即上方向)作为“上”方向。
如果你希望更好地控制对象的朝向和倾斜,可以传入第二个参数,指定一个自定义的上方向
Vector3 direction = new Vector3(1, 0, 0);
Vector3 up = Vector3.up; // 指定上方向为 Y 轴正方向
Quaternion targetRotation = Quaternion.LookRotation(direction, up);
意思是第一个向量确定xz面,上方向再加上y的确认,达成四元数
摄像机的跟随
游戏资源的关系梳理
写代码的时候更带一点编程人员的角度,可拓展性有时也会提高,比如对血条的控制,在这里,maxhp也同样加入了可以控制的范围,看上去多传入了一个参数可能会麻烦一点,不够简化,
但是多传入一个参数的性能消耗其实并不高,如果考虑到游戏制作流程的人力资源分配,则是小巫见大巫
细节之处的地方没有必要那么省性能,影响开发速度
对于做一个流程的游戏,如果过多把写了的东西揉成一团丢一边,然后重写,追求更好的写法,这样也不利于写联通的逻辑。
需要实现功能时,减少性能或优雅追求的妥协点,毕竟需要的功能之后再加上也是可以的,不需要的功能后面要砍也是可以的,而同一个功能的反复修改,可能会有一些得不偿失
gizmo如果没有打开,看不到寻路烘焙
需要注意的是,在unity开发中的“资源自带区”,完全不仅仅是只能看做对资源的提供,还有对变化数据资源的提供再提供,意思是同样的一份数据,可以仅仅是一存一拿,也可以是存入,被其他的拿去计算后,再拿回,
如装备的数据,但这只是很浅一部分,里面记录了很多游戏内容相关的东西,然后这些“游戏内容承载体”可让玩家的可操作数据因此发生二次变化,一种不属于物理操作本身的额外游戏内容数据变化
ps:物理操作本身给出的变化,称为一次变化,这种变化是固定的,与游戏额外添加内容无关的
将资源区的数据分为游戏本身自带的“资源自带区”,和玩家操作产生变化的区域
玩家操作产生变化的区域,
再分成如音乐大小设置,画质帧率设置,操作界面调整等偏后台类数据记录
和玩家操作过程中时常变动的数据,比如血量等级,经验值,装备穿戴情况(装备情况由 “资源自带区”提供,参与计算并给定最终对角色的影响)
资源的使用和游戏物体的预设使用,资源是直接可以套上组件,游戏对象的使用是对资源区的物体,执行实例化的方法
物体的删除和创建没有想象的那么耗性能,并不是说可以完全不要去注意,只是不要放过多权重在上面
SQL视图和索引
MySQL 视图(View)是一个虚拟表,它是基于 SQL 查询的结果集。视图并不存储数据本身,而是存储了一个 SQL 查询的定义。
CREATE VIEW order_summary AS
SELECT
order_id,
customer_id,
order_date,
total_amount
FROM
orders;
现在,可以简单地查询视图,SELECT * FROM order_summary;
视图可以将复杂的查询逻辑封装在一个简单的结构中。这样,你可以用一个简单的 SELECT 语句来替代复杂的 SQL 查询,降低了使用的复杂性。
视图可以被设计为可更新的,例如,如果你有一个简单的视图只选择了单个表的所有列,那么你可以通过视图进行数据更新,并且更新会直接影响基础表的对应记录。
UPDATE order_summary SET total_amount = 150.00 WHERE order_id = 1;
使用视图可以限制用户的访问权限。例如,如果你希望用户只能访问订单的摘要信息而不允许访问原始表中的其他敏感信息,可以创建一个视图,只包含所需的字段
非聚集索引是一种用于提高数据库查询性能的重要工具。它通过创建独立的索引结构,使得对表中特定列的查询更加高效。
如果没有索引,数据库需要扫描整个表以找到匹配的行;而有了 idx_department
索引后,查询会更快,因为数据库可以直接访问相关的索引数据。
SELECT * FROM employees WHERE department = 'Sales' AND salary > 50000;
如果 id
是主键,它会自动成为聚集索引。
父子预设体的问题
UI的子物体的预设体设置
Image 的 RectTransform 会记录其相对于 Canvas 的位置、锚点、大小等信息。当你在 Canvas 下创建 Image 时,这些属性都是相对于该 Canvas 的。
如果将 Image 预设体拖放到不同大小的 Canvas 上,由于其属性是相对原始 Canvas 的,所以会出现问题
位置偏移:由于不同 Canvas 的尺寸和锚点设置,Image 可能不会在新 Canvas 中显示在预期的位置。
缩放问题:如果原始 Canvas 和目标 Canvas 的缩放比例不同,Image 的大小和显示效果可能也会受到影响。
正常 的3d父子物体,子物体设置成预设体也是记录相对父物体的信息
子物体的 Transform 会记录相对于父物体的位置信息(localPosition)、旋转信息(localRotation)和缩放信息(localScale)。这意味着无论父物体的位置和旋转如何变化,子物体的相对位置和旋转关系保持不变。
如果没有父物体,则默认把这些相对的值赋到世界坐标系上
相对值:没有父物体的情况下,GameObject 的 localPosition
、localRotation
和 localScale
的值会直接对应于它的 position
、rotation
和 scale
。这意味着:
localPosition
等于position
localRotation
等于rotation
localScale
等于scale
当你将一个没有父物体的 GameObject 作为预设体存储并实例化时,它将出现在场景中的确切位置(相对于世界坐标系),并且其 Transform 信息不会受到其他物体的影响。
动画设置总结
串面板的实现,一定会存在初逻辑做的差不多之后发现有小问题没有统一的解决,
对于这种做的快完的时候发生的优化问题,如果想要完全避免的话,
思考成本很高,而且自己的功能实现能力也许会受限
而且一般来讲,这种问题都是边边角角的,如果需要额外加也是可以的
状态机的override版本一定是有用的,给的资源,角色拿着不同武器的移动方式,攻击方式是可归类的,
状态机合理利用override的话,应该说是对角色所有动作的构成和融合的归类,因为有blend tree
移动相关的设置可以更加概括成一个状态,
向前,后,左,右四个动作,构成一个blend tree 进行融合,并且用animator的参数管控blend的2d融合参数,
然后攻击动画只影响上半身,于是利用了遮罩的设置,先建立新的动画层,权重拉高,然后把下半身全部mask,触发开火的时候就可以只变化上半身,但这个新层要注意做一个空状态,不然有点没起点的感觉
对于下蹲,则是又开一层,但这一层开启sync同步,并贴在移动的基层上
之后通过new Layer的权重增大,从而达到让角色下蹲的样子
状态机的设置,可以在内部实现把移动的代码相关调控问题直接解决,给外部仅仅提供一个x和y值和触发的状态,就可以达成预期效果
Animator状态机的要点
层的同步
层的权重设置
层的遮罩不处理
层的参数外提供
状态的连接建立
参数对状态切换的调控
子状态机
右击子状态机或其他状态,选择 "Make Transition" 来创建过渡。
进入子状态机后,会进行独立的状态逻辑判断。子状态机内部可以有自己的状态、过渡和条件,这些都是独立于主状态机的。
可以把子状态机当成状态机的递归,在最上层的状态结构里,子状态机就是一个正常节点,但是如果过渡到该节点之后,就会变成在该子状态机了进行全新的条件判断
摄像机的动画设置
准确来说,有时候代码简化反而可能不如不简化直接使用,比如这里,很直接的把动画播放结束的事件分割成了一个只写了一点点代码的函数,看上去的确是可以简化,反正需要的效果也不多
但如果要简化,很容易把函数结束的一种逻辑潜意识也混入了左转右转的两个动画里,这对于保持逻辑清晰来说,简化后并不一定真的有简化,也许现在看不出什么问题,但有些问题会自然的带到后面对功能的开发
这里更像是动态添加事件函数,每次点击传入这段函数,然后转到动画事件部分,通过将传过来的时间,丢入一个事件变量,然后由PlayOver统一执行这个事件
这样的好处,首先的确是很清爽,如果想自己换法子实现,就要比较复杂的实现延时执行面板的显隐。
动画事件串联全程
添加之后会去脚本里面找
这个函数是写在挂在CameraAnimator这个自定义的脚本上的
动画事件那一块是可以检测到并使用的
动画完成之后要做什么操作,这里是设置了私有变量,以达到外部传入操作到Camera,Camera可以执行该操作
最后就是给外部调用的两个turn的函数,外部尽管传入操作,Camera内部会接收,先执行转向的动画,animator.SetTrigger("具体的对应状态");
然后Animatior就会进入动画的播放,而左转右转这两个自己编辑出来的动画,也对应的在播放结束的位置加上了动画事件,则是PlayOver
而此时,由于触发时间的不同,点击左转时,调用了左转的函数,
先是触发了左转动画的播放,然后立刻给动画播放完成之后 那个存储动画事件的变量添加了要执行的操作,等到旋转动画播放完,就执行那个存储的PlayOver函数,执行完之后再置空保证优雅
Animation Event
public void PlaySound(float volume)
{
Debug.Log($"Playing sound at volume: {volume}");
}
挂载到该 GameObject 的 MonoBehaviour 脚本中定义。被调用的函数必须是 public
的。
Animation Event 中,还可以用带参数的函数,难免就会有疑惑,因为方法是写成这样,而调用传参的权利不在自己而在Animation event上。
参数值是由你在 Animation Event 的设置面板中直接定义的,在参数字段中,输入你想要传递给 volume
的具体值,比如 1.0
。这就是你在 Animation Event 中设置的参数值,是你当场定义当场用的感觉,
参数并不是自动从其他地方传递的,而是通过你在 Animation Event 中配置的内容直接给定的。。
背景音乐设置
使用UImanager来管理面板的显隐
GameDataMgr来统一管理数据的存储
音乐脚本设置为单例,强调只需要一个来管理就够了
节约性能,虽然实时在改变拖动的值,但是并没有存在musicdata里面,在点击叉之后再一次性存入Gamedatamgr
每次滑动都直接修改音量上的值,和游戏数据管理类里的直接值,那这里的SaveMusicData
意味着存入json?是的,毕竟内存和硬盘看似很接近,实则天差地别,两个完全不属于一个世界
从硬盘里通过json读过来的数据,到了内存里就和之前毫无联系了,
只有内存呼唤json,建立联系,才能把内存的数据再输入给硬盘
对于背景音乐,设置一个场景上的对象,再挂载对应的脚本,管理上面的各种声音
数据管理类的单例,首次调用时引发构造函数,构造函数中导入json数据
而音乐对象身上的awake里,则调用游戏管理数据类获取音乐设置数据
Life is an absurd art.