【Unity】Playable使用细则

【Unity】Playable使用细则

本文基于Unity 2021.3 API。

本文介绍官方文档中没提及的Playable使用限制、注意事项、Bug及规避方案,不是Playable的入门教程!
如果你还不熟悉Playable的基础用法,请先学习以下官方文档和示例:

  • PlayableGraph介绍
  • Playable介绍
  • Playable API文档
  • ScriptPlayable API文档
  • PlayableBehaviour API文档
  • AnimationScriptPlayable API文档
  • IAnimationJob API文档
    • NativeArray API文档
    • NativeList API文档
    • NativeParallelHashSet API文档
    • Unity Collections包手册
  • AnimationStream API文档
  • PropertyStreamHandle API文档
  • PropertySceneHandle API文档
  • TransformStreamHandle API文档
  • TransformSceneHandle API文档
  • Notification
  • Unity-Technologies/SimpleAnimation示例项目
  • Unity-Technologies/animation-jobs-samples示例项目

PlayableGraph Monitor

一个PlayableGraph监控工具,功能比Unity官方的Graph Visualizer更强更完善。

  • 支持大型PlayableGraph
  • 可显示PlayableGraph和Playable节点的详细数据
  • 支持缩放视图
  • 支持拖拽视图和Playable节点
  • 支持为Playable节点添加额外文本标签
  • 支持带有循环引用的PlayableGraph(需要手动调整节点布局)

工具地址: https://github.com/SolarianZ/UnityPlayableGraphMonitorTool

PlayableGraph Monitor

PlayableGraph

连接Playable和Playable Output时不需要严格的匹配Playable类型,一个Playable可以同时作为多个 ScriptPlayableOutput 的输入。动画Playable可以连接到脚本Playable,也可以作为 ScriptPlayableOutput 的输入,反之亦然。但需要注意,如果Playable最终没有输入到对应类型的 ScriptPlayableOutput ,其中的某些功能可能不会生效,下文有具体案例。

在Editor中逐帧运行游戏时, PlayableGraph.IsPlaying() 方法总是返回 false ,无论是否调用过 PlayableGraph.Stop() 方法。我认为这是个Bug,但是Unity表示这是故意设计的。这个设计显然非常糟糕,因为Runtime应该对Editor无感知,逐帧播放是存粹的Editor功能,但它却改变了Runtime接口的行为。如果游戏中根据PlayableGraph的播放状态决定代码执行逻辑,那在逐帧调试时很可能出现异常。要规避此问题,可以额外维护一个字段来标识PlayableGraph是否被人为停止播放。

PlayableGraph开始播放后,将其更新模式设为 DirectorUpdateMode.Manual ,PlayableGraph的播放状态会自动变为停止( PlayableGraph.IsPlaying() 方法返回 false )。此时再调用 PlayableGraph.Play() 方法,PlayableGraph的播放状态会变为播放中( PlayableGraph.IsPlaying() 方法返回 true ),但PlayableGraph仍不会自动更新,需要主动调用 PlayableGraph.Evaluate() 方法驱动其更新,至此都是符合预期的。此时,如果将PlayableGraph的更新模式设为任意 DirectorUpdateMode.Manual 模式,PlayableGraph将不会按预期恢复自动更新。这是 Unity的Bug ,要使PlayableGraph恢复自动更新,可以先调用 PlayableGraph.Stop() 方法,再调用 PlayableGraph.Play() 方法,强制刷新一下状态。参考下文的Bug规避方案。

ScriptPlayable<T>和PlayableBehaviour

PlayableBehaviour 的生命周期如下图所示:

PlayableBehaviour Lifecycle

PlayableGraph在每一帧中总是先前序遍历调用每个节点的 PrepareFrame() 方法,再后序遍历调用每个节点的 ProcessFrame() 方法。如果 ScriptPlayable<T> 最终没有输出到 ScriptPlayableOutput ,其 PlayableBehaviour.ProcessFrame() 方法 不会 被调用,但其 PlayableBehaviour.PrepareFrame() 方法 被调用。

Initialize() 方法不是 PlayableBehaviour 内置的生命周期方法。为了实现自定游戏逻辑,一般需要给Playable传递一些数据来对其进行初始化,因此通常会额外定义一个 Initialize() 方法,在创建Playable后立即调用此方法来进行初始化。参考示例代码。

// PlayableBehaviour初始化示例
public class MyBehaviour : PlayableBehaviour
{
    private object _data;

    public void Initialize(object data)
    {
        _data = data;

        // TODO: 其他初始化操作……
    }

    // TODO: 其他生命周期方法……
}

// 示例方法:创建ScriptPlayable并初始化Behaviour
public ScriptPlayable<MyBehaviour> CreateMyScriptPlayable(PlayableGraph graph, object data)
{
    var playable = ScriptPlayable<MyBehaviour>.Create(graph);
    var behaviour = playable.GetBehaviour();
    behaviour.Initialize(data);
    return playable;
}

动画Playable的评估(Evaluate)顺序

PlayableGraph在每帧中总是按照后续遍历的顺序评估每个动画Playable。在 AnimationScripPlayable 的Job中,如果手动触发对子Playable树的评估时没有按输入索引顺序升序进行,这个规则将在这一节点中被破坏。

AnimationClipPlayable

使用 AnimationClipPlayable 播放 非循环 动画时:

  • 将时间设置到大于AnimationClip长度的位置
    • 若播放速度大于 0 ,动画不会继续播放
    • 若播放速度小于 0 ,动画会反向播放直到时间为 0
  • 将时间设置到小于 0 的位置
    • 若播放速度小于 0 ,动画不会继续播放
    • 若播放速度大于 0 ,动画会正向播放直到时间为AnimationClip长度

AnimationScriptPlayable

AnimationScriptPlayable 用于在多线程环境中执行自定义动画Job。在自定义动画Job中,可以通过 AnimationStream 来读写动画和组件数据,实现程序性动画。

定义动画Job时,需要实现一个实现了 IAnimationJob 接口的纯值类型结构体,该结构体中不能直接或间接含有任何引用类型的非静态字段或非静态属性。Unity提供了一些非托管的集合和引用,可在一定程度上减少不能使用引用类型所带来的限制。参考上文提及的NativeArray和Unity Collections包。

Animator.cullingMode 不是 AnimatorCullingMode.AlwaysAnimate ,当角色不在相机视锥体内时, IAnimationJob.ProcessAnimation() 方法 不会 被调用。设计如此,符合预期。

AnimationScriptPlayable 没有最终没有输入到 AnimationPlayableOutput ,或者所输入到的 AnimationPlayableOutput 没有绑定到有效的 Animator 组件, IAnimationJob.ProcessRootMotion() 方法和 IAnimationJob.ProcessAnimation() 方法都 不会 被调用。设计如此,符合预期。

在某些Playable连接关系下, IAnimationJob.ProcessRootMotion()IAnimationJob.ProcessAnimation() 不会被调用,是 Unity的Bug ,参考下文的Bug规避方案。

设置 AnimationScriptPlayable 的输入权重不会实际影响输入Playable的数据,需要在Job代码中手动处理权重。在手动处理输入权重的情况下,一般会使用 AnimationScriptPlayable.SetProcessInputs(false) 方法来禁止其自动评估子Playable树,然后在Job代码中调用 AnimationStream.GetInputStream() 方法来手动触发评估子Playable树。因为 AnimationScriptPlayable 自动评估所得到的输入数据会被直接写入到自己的 AnimationStream 中,没有施加权重影响,属于无用的数据,浪费计算性能。另外 AnimationStream.GetInputStream() 方法没有内部缓存机制,每次调用都会重新评估整棵子Playable树,应该尽量减少调用次数。参考示例代码。

// 自定义动画混合器示例
public struct MyCustomMixerJob : IAnimationJob
{
    public NativeArray<TransformStreamHandle>.ReadOnly boneHandles;

    public void ProcessRootMotion(AnimationStream stream) { }

    public void ProcessAnimation(AnimationStream stream)
    {
        // 每次GetInputStream,都会触发对输入子树的评估,开销很高,
        // 所以这里先缓存输入流,不在每个骨骼循环中反复调用GetInputStream
        Span<AnimationStream> inputStreams = stackalloc AnimationStream[stream.inputStreamCount];
        for (int i = 0; i < stream.inputStreamCount; i++)
        {
            inputStreams[i] = stream.GetInputStream(i);
        }

        for (int i = 0; i < boneHandles.Length; i++)
        {
            var boneHandle = boneHandles[i];
            for (int j = 0; j < inputStreams.Length; j++)
            {
                var inputStream = inputStreams[j];
                // TODO: 在这里完成自定义混合逻辑,例如惯性混合……
            }
        }
    }
}

// 示例方法:创建自定义动画混合器Playable(示例中没有连接输入的子Playable树)
public AnimationScriptPlayable CreateMyCustomMixerPlayable(PlayableGraph graph,
    NativeArray<TransformStreamHandle>.ReadOnly boneHandles)
{
    var jobData = new MyCustomMixerJob
    {
        boneHandles = boneHandles,
    };
    var playable = AnimationScriptPlayable.Create(graph, jobData);
    // 禁止自动评估输入的Playable子树,这样在Job中手动调用GetInputStream之前,整棵子树都不会被评估
    playable.SetProcessInputs(false);

    return playable;
}

AnimationStream

AnimationStream 作为动画数据的载体在动画Playable之间传递。在 IAnimationJob 中,可以修改 AnimationStream 中的动画数据,实现程序性动画。

AnimationStream

修改 AnimationStream.velocity 属性可以改变角色移动速度。修改 AnimationStream.angularVelocity 属性可以改变角色的转向速度(单位是弧度/秒)。这两个速度都是模型空间下的速度,使用时可能需要进行坐标空间转换。

AnimationStream 配合 PropertyStreamHandle/PropertySceneHandleTransformStreamHandle/TransformSceneHandle 可以实现读写动画曲线、组件属性和 Transform 数据,下文会介绍。

PropertyStreamHandle和PropertySceneHandle

PropertyStreamHandle 可以绑定到 动画曲线 、组件属性和自定义属性,然后借助 AnimationStream 读写所绑定的属性值。目前支持 floatintbool 类型的属性。需要注意, PropertyStreamHandle 所绑定的目标 Component 对象必须是 AnimationPlayableOutput 所绑定的 Animator 组件所在的GameObject的直接或间接子节点。

组件属性被绑定到 PropertyStreamHandle 后,将无法在动画Job外部修改其数值,是 Unity的Bug ,已在Unity 2022.2.17f1中修复。

通过 PropertyStreamHandle 修改 “GravityWeight” 曲线的值,无法实际影响到角色所承受的重力(作用于 CharacterController 组件),官方回复说不是Bug,设计如此。

在某些Playable连接关系下,通过 PropertyStreamHandle 修改属性不会生效(或数值不匹配),是 Unity的Bug ,参考下文的Bug规避方案。

AnimatorJobExtensions.BindStreamProperty() 方法同时支持绑定动画曲线、组件属性和自定义属性:

  • 绑定组件属性时,目标组件不能是 Transform ,否则会报错( Transform 需要使用 TransformStreamHandle 绑定)
  • 绑定组件属性时,目标属性名必须在目标组件中存在,否则会报错
  • 绑定AnimationClip中的动画曲线或自定义属性时, transform 参数是 Animator.transformtype 参数是 typeof(Animator)
    • 如果属性名参数 property 与AnimationClip中的动画曲线名称相同,则绑定动画曲线,否则绑定为自定义属性
    • AnimationClip中的动画曲线中可能有 BlendShape 数据,但是BlendShape是 SkinnedMeshRenderer 组件的属性,绑定时需要指定组件类型为 typeof(SkinnedMeshRenderer) ,不是 typeof(Animator)
  • 绑定AnimationClip中的动画曲线时,需要删除曲线名称中的空格
  • 绑定在 动画导入设置 中手动添加的曲线,需要保留曲线名称中的空格
  • 某些自定义动画曲线需要在模型导入设置中启用 Animated Custom Properties 选项后才能绑定
  • 并非所有的动画曲线都能绑定(例如,AnimationClip中的肌肉曲线和带路径的曲线无法绑定),参考下方的获取可绑定的动画曲线的名称的代码

AnimatorJobExtensions.BindCustomStreamProperty() 方法只能用于绑定自定义属性,不能绑定动画曲线和组件属性:

  • 该方法总是在动画内存中开辟新空间存储目标属性,即使AnimationClip中有同名曲线,也不会绑定到该曲线
// 获取可绑定的动画曲线的名称
public static List<string> GetBindableCurveNames(AnimationClip clip)
{
    List<string> exclusion = new List<string>() {
        "RootT.x", "RootT.y", "RootT.z",
        "RootQ.x", "RootQ.y", "RootQ.z", "RootQ.w",
        "LeftFootT.x", "LeftFootT.y", "LeftFootT.z",
        "LeftFootQ.x", "LeftFootQ.y", "LeftFootQ.z", "LeftFootQ.w",
        "RightFootT.x", "RightFootT.y", "RightFootT.z",
        "RightFootQ.x", "RightFootQ.y", "RightFootQ.z", "RightFootQ.w",
        "LeftHandT.x", "LeftHandT.y", "LeftHandT.z",
        "LeftHandQ.x", "LeftHandQ.y", "LeftHandQ.z", "LeftHandQ.w",
        "RightHandT.x", "RightHandT.y", "RightHandT.z",
        "RightHandQ.x", "RightHandQ.y", "RightHandQ.z", "RightHandQ.w",
    };

    var curveNames = new List<string>();
    var muscleNames = new List<string>(HumanTrait.MuscleName);
    var curveBindings = UnityEditor.AnimationUtility.GetCurveBindings(clip);
    foreach (var binding in curveBindings)
    {
        if (!string.IsNullOrEmpty(binding.path))
        {
            continue;
        }

        if (muscleNames.Contains(binding.propertyName))
        {
            continue;
        }

        if (exclusion.Contains(binding.propertyName))
        {
            continue;
        }

        const string LeftHandPrefix = "LeftHand.";
        if (binding.propertyName.StartsWith(LeftHandPrefix))
        {
            var propName = "Left " + binding.propertyName.Substring(LeftHandPrefix.Length).Replace('.', ' ');
            if (muscleNames.Contains(propName))
            {
                continue;
            }
        }

        const string RightHandPrefix = "RightHand.";
        if (binding.propertyName.StartsWith(RightHandPrefix))
        {
            var propName = "Right " + binding.propertyName.Substring(RightHandPrefix.Length).Replace('.', ' ');
            if (muscleNames.Contains(propName))
            {
                continue;
            }
        }

        curveNames.Add(binding.propertyName);
    }

    return curveNames;
}

PropertySceneHandle 的功能与 PropertyStreamHandle 类似,但只提供数据读取功能,不支持数据写入,并且可以绑定到场景中的任意 Component 对象,不受与 Animator 组件的层级关系限制。

TransformStreamHandle和TransformSceneHandle

TransformStreamHandle 可以绑定到一个 Transform 组件,然后借助 AnimationStream 读写 Transform 数据。需要注意, TransformStreamHandle 所绑定的目标 Transform 组件必须是 AnimationPlayableOutput 所绑定的 Animator 组件所在的GameObject的直接或间接子节点(包含自身,但绑定自身时可能遇到无法修改角色位置或角色跳回初始位置的问题,是 Unity的Bug ,参考下文的Bug规避方案)。

TransformSceneHandle 的功能与 TransformStreamHandle 类似,但只提供数据读取功能,不支持数据写入,并且可以绑定到场景中的任意 Transform 组件,不受与 Animator 组件层级关系限制。

执行时序变化

动画Job方法的调用时机、动画消息的触发时机,会受到 Animator.updateMode 属性和 PlayableGraph 的更新模式( DirectorUpdateMode )影响。

Animator.updateModeAnimatorUpdateMode.NormalPlayableGraph 的更新模式为 DirectorUpdateMode.GameTime 时, ScriptPlayable<T> 先于动画Job进行准备和评估。利用这一点,可以将动画Playable输入到Script Playble,在评估动画之前,在ScriptPlayable的 PrepareFrame() 方法中完成动画的逻辑状态更新(修改权重、接枝、剪枝等)。

Playable Notification在推送后进入队列,而不是立即发送给监听者,当Playable Graph评估完成后,才会发送给监听者。

Playable中的deltaTime

AnimationStream.deltaTimeFrameData.deltaTime 的值不受 Time.timeScale 的影响,但是动画Playable在评估动画时已经代入了时间缩放和播放速度,在手动计算动画数据时,要注意判断是否需要代入这两个值。
FrameData.effectiveSpeed 会同时受到 Time.timeScale 和Playable播放速度的影响。可以用 FrameData.deltaTime * FrameData.effectiveSpeed 来计算这一帧中受到缩放影响后的deltaTime。

Playable Bug及规避方案汇总

Playable系统中有很多离谱的Bug。有些Playable在简单连接结构下做测试的时候,表现正常,但连接结构变得复杂后,就出Bug了。这个时候,你很可能已经基于原本预期表现正常的Playable做了很多上层封装,为了解决这个突然出现的Bug,不得不去改架构,非常恶心。这里的每一项注意事项和Bug记录,都是我踩过的一个坑!

  • Bug及规避方案汇总
  • Bug及规避方案的最小化复现工程汇总

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/25662.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于STM32的定时器--定时中断(HAL库)

基于STM32的定时器--定时中断&#xff08;HAL库&#xff09; 介绍引言定时器介绍 实例项目介绍准备设计流程 介绍 引言 本文旨在介绍如何使用STM32CubeMX配置KEIL 5开发一个每10us定时器中断触发一次的项目。帮助初学者入门STM32的定时器使用。 定时器介绍 定时器是STM32微…

chatgpt赋能python:Python升降序排列数字

Python升降序排列数字 在Python编程中&#xff0c;排序是一个非常常见并且重要的操作。Python提供了多种排序算法以满足不同的需求。 排序算法 Python中内置的排序算法有两种&#xff1a;Timsort和Quicksort。其中Timsort是一种混合排序算法&#xff0c;结合了插入排序和归并…

Linux系统中源码安装1.8.x版本Arduino IDE

本文内容参考&#xff1a; Ubuntu22.04安装Arduino IDE及Arduino UNO&#xff08;使用CH341驱动&#xff09;调试方法__KILLMILEDC_的博客-CSDN博客 在Linux上下载arduino_不说话的白帽子的博客-CSDN博客 https://guoqing.blog.csdn.net/article/details/88913063?spm1001.…

Linux NGINX服务 ReWrite^location

ReWrite^location 从功能看 rewrite 和 location 似乎有点像&#xff0c;都能实现跳转&#xff0c;主要区别在于 rewrite 是在同一域名内更改获取资源的路径&#xff0c;而 location 是对一类路径做控制访问或反向代理&#xff0c;还可以proxy_pass 到其他机器。 rewrite 对访问…

c++ new 源码学习一下

之前有一篇文章介绍了 new 的一些用法 c new 在指定内存上创建对象&#xff0c;今天结合源码来学习一下 new 更详细的用法。相关的源码&#xff1a;gcc git 1&#xff0c;void* operator new (std::size_t size); 我们可以在头文件<new>里看到它的原型&#xff1a; _G…

C++11 -- lambda表达式

文章目录 lamaba表达式的引入lambda表达式语法lamabda达式各部分说明捕获列表说明 lamaba表达式底层原理探索 lamaba表达式的引入 在C11之前,如果我们想对自定义类型Goods排序,可以根据姓名,价格,学号按照从大到小或者从小到大的方式排序,可是,这样我们要写额外写6个相关的仿函…

Quest 3初体验,或是苹果MR最大竞争对手

随着苹果MR临近&#xff0c;我们从彭博Mark Gurman了解到更多消息。昨日&#xff0c;Mark Gurman发布了Quest 3上手体验文章&#xff0c;并认为Quest 3可能是苹果MR头显最大的竞争对手。 1&#xff0c;Meta是XR头显领导者 尽管WWDC 23苹果MR将会成为最大的主角&#xff0c;但…

node.js与内置模块

一、目标 能够知道什么是Node.js能够知道Node.js可以做什么能够说出Node.js中的JavaScript的组成部分能够使用fs模块读写操作文件能够使用path模块处理路径能够使用http模块写一个基本的web服务器 二、目录 初始Node.jsfs文件系统模块path路径模块http模块 1.初始Node.js …

macos wireshark 抓取https包

1、启动浏览器 1.1 创建空文件 $ touch /Users/zhujl/Downloads/https/mysslkey.log 2、设置wireshark tls属性&#xff0c;指定tls密钥存储文件 2.1 进入Wireshark Preferfences > Protocols > TLS 属性配置 2.2 勾选上Reassemable TLS records spanning multiple …

设计模式B站学习(一)(java)

这里写目录标题 一、设计模式概述1.1 软件设计模式的产生背景1.2 软件设计模式的概念1.3 学习设计模式的必要性1.4 设计模式分类 二、UML图2.1 类图概述2.2 类图的作用2.3 类图表示法2.3.1 类图表示方法2.3.2 类与类之间关系的表示方法2.3.2.1 关联关系2.3.2.2 聚合关系2.3.2.3…

Selenium的使用

一、基础 1、特点 selenium 是web中基于UI的自动化测试工具&#xff0c;它支持多平台、多语言、多浏览器&#xff0c;还有丰富的API。 2、原理 自动化脚本代码会创建一个http请求发送给浏览器驱动进行解析&#xff0c;浏览器驱动会操控浏览器执行测试&#xff0c;浏览器接着…

ffmpeg编译成wasm

最近在看ffmpeg的源码 https://ffmpeg.xianwaizhiyin.net/ffplay/ https://crifan.github.io/media_process_ffmpeg/website/audio_process/ 做个可运行的例子 代码在找了一堆&#xff0c;可用的版本放在这 https://github.com/killinux/ffmpeg_wasm_demo 先把ffmpeg 编译成 …

内蒙古自治区出台加快充换电基础设施建设实施方案

摘要&#xff1a;为深入贯彻落实《国务院办公厅关于印发新能源汽车产业发展规划&#xff08;2021—2035年&#xff09;的通知》&#xff08;国办发 ﹝2020﹞39号&#xff09;、《国家发展改革委等部门关于进一步提升电动汽车充电基础设施服务保障能力的实施意见》&#xff08;发…

Unity——在C#中调用C++动态链接库(DLL)

一、创建C动态链接库&#xff08;DLL&#xff09; 1、新建C空项目 打开VS&#xff0c;新建一个C空项目&#xff0c;自命名项目名称与位置。 2、配置项目属性为动态链接库 右键项目&#xff0c;点击属性&#xff0c;打开项目属性页&#xff0c;将常规中的配置类型改为动态库&…

电力电子技术的论文

电力电子技术的论文范文一&#xff1a;Matlab电力电子技术应用 【文章摘要】信息技术的快速发展推动许多学科进一步完善&#xff0c;以电力电子技术为例&#xff0c;其本身具有较强的理论性、实践性等特征&#xff0c;涉及的波形图、电路图也较多&#xff0c;相关设计人员需掌握…

【C++初阶】C++STL详解(一)—— string类

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C初阶 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 CSTL详解&#xff08;一…

新华三的网络脉动:为AI泵血,向产业奔流

AI大模型作为最新的通用技术&#xff0c;今年以来&#xff0c;发展如火如荼。也有很多从业者和专家注意到&#xff0c;AI模型训练和应用过程中&#xff0c;需要优先考虑网络的升级与适配。 如果说数据中心、算力集群是AI的“心脏”&#xff0c;那么网络就犹如AI的“动脉”&…

综合指挥调度系统行业分类汇总

综合指挥调度系统是将语音、视频、GIS进行高度融合&#xff0c;构建“平战结合”的指挥调度模式&#xff0c;既满足平时的应急培训、日常通信、会议会商等要求&#xff0c;也能够应对战时的应急指挥、应急救援、应急决策等需求&#xff0c;达到统一指挥、联合行动的目的&#x…

ArduPilot之H743+BMI270x2+First Normal Takeoff

ArduPilot之H743BMI270x2First Normal Takeoff 1. 源由2. 正常起飞3. 问题汇总3.1 机架构型3.2 IMU对齐3.3 接收机3.4 GPS3.5 VTX3.6 电调3.7 PID 4. 总结5. 参考资料6. 附录6.1 补充AcroTrainer视频6.2 补充Acro视频 1. 源由 鉴于目前该飞控板子在ArduPilot开源社区尚未得到官…

15.2:分金条的最小代价

一块金条切成两半&#xff0c;是需要花费和长度数值一样的铜板 比如长度为20的金条&#xff0c;不管怎么切都要花费20个铜板&#xff0c;一群人想整分整块金条&#xff0c;怎么分最省铜板? 例如&#xff0c;给定数组{10,20,30}&#xff0c;代表一共三个人&#xff0c;整块金条…