updater 文档 | 第 3 节 基于时间的更新
从时间轴的角度,直接控制 manim 动画的流程
引入
之前我们提到了有起始和终止的动画,以及无限播放的动画。这一节我们将探讨后者应当如何编写。
试试看!
编写一个时钟,使得秒针、分针、时针分别按照固定的角速度旋转。如果严格按照现实时间来写比较慢,可以适当增加角速度。
如果上面的例子你能写出来,那么这一节就结束了。
开个玩笑
浅析基于时间的 updater
我们回到上一节提到的基于时间的更新函数,它的类型定义如下。此时这个函数接收的参数是两个,一个是 Mobject ,另一个是 dt 。
同时,在 add_updater
方法中,有这么一条语句:
很明显,当更新函数的参数中如果有一个名字为 dt ,那么就会执行下面的步骤,即将基于时间的更新函数添加到成员列表中。接下来就只需要接着上一节的分析,在每次 动画播放 的时候,动画过程中的每一帧都会调用这一函数,对物件进行更新了。
然而我们好像忽略了 wait
这个方法。我们发现,基于时间的更新不管是遇到 play
还是 wait
,它都会更新。
可以看到,wait
方法中,其实也是做了一些关于 updater 的处理,这样就能做到 wait
时也能处理基于时间的更新了。
玩笑归玩笑,时钟的例子还是得给答案的
dt 的值是多少
如果你运行了上面的例子就会发现, 10 秒钟的等待时间,秒针刚好就走了 10 个刻度。这看似是个巧合,但是我们可以使用 print 来 debug 呀。如果尝试在更新函数中加入一句 print(dt)
,那么会打印一堆小数。这些小数大多都接近 0.0166 ,这是因为 dt 就是帧率的倒数。也就是说,每一秒钟,dt 在这一区间段的总和就是 1 。
为什么要有这样一个设定呢?我们用常数代替 dt 不行吗?
当然可行,毕竟 dt 就是一个浮点数。但是当全局设定的帧率变了,而我们仍然使用我们定义的常量,就会导致不同帧率情况下,动画的结果就不同。
但是,如果我们使用 dt ,并且在使用的过程中有意地给某个变化量乘上 dt ,那么在单位时间内,这个变化量就是恒定的了。
比如我们将秒针的更新函数改成这样
那么,在 60 帧的视频中,每 10 秒钟,秒针就会走 10 个刻度。而在 30 帧的视频中,每 10 秒钟,秒针只会走 5 个刻度,因为在 30 帧的视频中,更新函数在每秒只会被调用 30 次。所以,dt 可以作为一个标准化的数值来使用。
仔细阅读的读者应该也能发现, dt 是通过当前帧的“时间戳”减去上一帧的“时间戳”来算出来的。既然有这个“时间戳”,我们能把它利用起来吗?当然是可以的, Scene 有一个成员变量 time
就存放了当前动画已经运行的秒数。我们可以用 DecimalNumber
来将它显示在屏幕上。
更多的例子
1. 恒星行星模型:制作地球绕太阳公转,月球绕地球公转的动画。公转的速度、半径不做硬性要求。
参考解答
2. 李萨如曲线:让一个物件按照李撒如图形的轨迹持续运动。李萨如图形的参数可自己给定。
参考解答
在这里同样也用了 self.time
这个变量,处理更加方便一些
3. 追逐曲线:一个正三角形的三个顶点分别以一定速度靠近同一侧相邻的顶点,画出每一帧三角形的位置。
参考解答
4. 东方弹幕游戏:对手每一帧都会改变弹幕的发射角,但是当弹幕发射出去之后,只会进行匀速直线运动,直至飞出屏幕。你需要制作一张螺旋符卡,拥有至少 3 条伪线,且发射角匀速变化。
参考解答
当然这里的例子我并没有进行优化,如果是游戏开发者,肯定了解过对象池的概念,它能有效优化对象频繁创建和析构带来的性能损失问题。
5. 东方弹幕游戏进阶:境符「波と粒の境界」可谓是观赏性极高的符卡之一,现在紫妈要求你用 manim 来实现它。
参考解答
其实只需要改第 6 行就可以了。只要发射角的变化是非线性的,就可以达到这样的效果。当然,如果觉得子弹还不够密集(因为 60 帧下每秒只有 60 颗)可以再加几组。
#manim
#updater
#教程