持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 30 天,点击查看活动详情
前言
这是一套 张风捷特烈 出品的 Flutter&Flame
系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】和 【pinball】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:
- 【Flutter&Flame 游戏 - 壹】开启新世界的大门
- 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
- 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
- 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
- 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
- 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
- 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
- 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
- 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
- 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
- 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
- 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
- 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
- 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
- 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
- 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
- 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
- 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
- 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
- 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
- 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
- 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
- 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
- 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
- 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
- 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
- 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
- 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
- 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机(
本文
)
1. 认识视口与相机
相机是我们日常生活中非常常见的概念,在 Flame
中,相机的概念如何理解呢?现实生活中,当你使用相机拍出一张照片,其囊括的区域是有限的,这个区域也就是视口 Viewport
。下面是 Flame
中对Camera
类的定义,其继承自 Projector
,且持有 Viewport
对象。
其中 Projector
是对投影的抽象,Flame
只是个二维的游戏引擎,所以投影的概念也很简单。就是对一个平面空间点位,进行操作,产出与之对应的点位而已。如下的 projectVector
方法的作用是:传入一个 Vector2
,进行变换后,产出一个 Vector2
。
简单来说,相机的作用是:在视口内对原本空间坐标信息
进行变换,完成对应的功能需求。注意,这里的 Camera
类和硬件设备的相机没有半毛钱关系。
2. 简单使用 Camera
FlameGame
中持有 CameraWrapper
对象,该对象内部持有 Camera
对象。如下箭头所示,FlameGame
中,可以通过 get camera
来访问相机。另外 FlameGame
的尺寸也是由相机决定的。
我们知道, 默认情况在 FlameGame
会填充整个窗口,而且背景是黑色的。
当窗口尺寸发生变化时,由于角色的坐标、尺寸等数据和逻辑像素是 1:1
的对应关系,也就是说坐标点没有进行过任何变换。所以角色的显示情况不会有任何变化:代码见 【29/01】
下面通过使用 FixedResolutionViewport
视口,实现固定视口尺寸的需求。此时游戏视口尺寸
和 窗口尺寸
就不是一个概念了。无论应用窗口有多大,对游戏而言视口尺寸是恒定的。如下白色背景构件添加到游戏场景中,布满视口,视口
会根据大小来适应窗口
,不在视口区域内的部分会显示底色。【29/02】
比如上图中默认相机的视口尺寸是 900*600
,并不是指白色区域的是 900*600
逻辑像素。另外,可以看到角色的尺寸没有改动,但在这个视口尺寸下,就会显得较小。为相机指定视口直接用 camera.viewport
指定即可。
---->[TolyGame]----
@override
Future<void> onLoad() async {
final Vector2 fixSize = Vector2(900, 600,);
camera.viewport = FixedResolutionViewport(fixSize);
add(Background());
add(HeroMan()..position=size/2);
}
复制代码
比如下面将视口指定为 90*60
,相对而言角色的尺寸就会变大。所以,从这里可以体会一下相机视口对于坐标系变换的特性。
此时改变窗口尺寸,通过打印日志可以发现, FlameGame
中的尺寸始终保持不变。这就是 FixedResolutionViewport
的作用,它可以保证在任何窗口尺寸下,游戏视口尺寸的恒定。也就是说,让游戏的可见部分在所有设备上都是相同的。
2. 相机的变换操作
相机的变换是针对于整个视口进行的,也就是说,可视区域内的角色呈现都会受到相机变换的影响。比如在现实生活中,当你移动相机,或者拉进、远离相机和目标的位置,都会影响最终的成像情况。
通过如下案例来说明一下相机变换操作对显示的影响:小人在中间,背景中左右各有 18
个原点。可以注意到,当圆点在视口之外,是无法显示的。就像相机拍照时,只能显示出其成像的区域。代码详见 【29/03】
相机缩放是比较简单的,对 camera.zoom
值进行改变即可:
if (event.logicalKey == LogicalKeyboardKey.keyZ && isKeyDown) {
camera.zoom = 0.1;
}
if (event.logicalKey == LogicalKeyboardKey.keyX && isKeyDown) {
camera.zoom -= 0.1;
}
复制代码
如下,通过减小 zoom
值,可以达到缩小的目的;就相当于照相机远离目标,从而成像区域可以包含更多内容,但视口中的内容也会相对变小。
同理,增加 zoom
值,可以达到放大的目的;就相当于照相机靠近目标,从而成像区域包含内容减少,但视口中的内容也会相对变大。简单来说,就是近大远小。
我们也可以对相机进行移动,从而改变成像区域的内容。Camera
中提供了 moveTo
和 snapTo
两个移动方法,分别表示动画移动
到某点和立刻移动
到某点。并且可以通过 camera.speed
设置移动的速度。
if (event.logicalKey == LogicalKeyboardKey.arrowUp && isKeyDown) {
camera.moveTo(Vector2(0, size.y/2-37/2));
}
复制代码
3.相机的伴随移动
相机伴随角色移动很好理解,比如现实生活中拍电影,摄像机需要跟随演员同步运动,这样才能保证演员在移动时常驻在视图中。官方的案例对这个知识点的说明比较好,这里就对它介绍一下。
场景中主要有 3
种构件:主角
、背景场地
和岩石方块
。场地是圆形和正方向构成的,颜色随机,其中圆形是正方向的内接圆。岩石随机出现在场地中,主角是一个动画帧。
如下所示,在角色移动过程中,始终保持在中心位置,但感官上它确实在运动。通过相机和角色的伴随移动,就可以始终让角色成为焦点,角色在移动的过程中,视口内容因相机的移动而扩展,这是符合我们常识的。代码详见 【29/04】
代码实现起来非常简单,只要调用 camera.followComponent
方法,指定需要跟随的构件即可。这样当构件的位置发生改变,相机也会随之变化。
@override
Future<void> onLoad() async {
final Vector2 fixSize = Vector2(500, 500,);
camera.viewport = FixedResolutionViewport(fixSize);
add(Ground());
add(ember = MovableEmber());
camera.speed = 1;
camera.followComponent(ember, worldBounds: Ground.bounds);
for (var i = 0; i < 30; i ) {
add(Rock(Vector2(Ground.genCoord(), Ground.genCoord())));
}
}
复制代码
该案例,当角色和岩石碰撞时,可以看出角色在视口区域的 中上方
,而且会动画平滑过渡;离开岩石后,又会在视口中间。在 MovableEmber
中可以看到碰撞逻辑,执行的是相机的 setRelativeOffset
方法。可以看出,相机的使用还是比较简单的。
@override
void onCollision(Set<Vector2> points, PositionComponent other) {
super.onCollision(points, other);
if (other is Rock) {
gameRef.camera.setRelativeOffset(Anchor.topCenter);
}
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
if (other is Rock) {
gameRef.camera.setRelativeOffset(Anchor.center);
}
}
复制代码
4. pinball 中相机的处理
pinball
中相机的行为被封装为 CharacterSelectionBehavior
构建,用于处理相机的行为。
如下所示,在点击 Play
时,场景会进行放大和移动。同样,游戏结束时也会有个类似的放大,移动到排行榜的位置。
对于不同的状态,操纵摄像机进行不同的处理,这里通过 _foci
映射来维护状态 GameStatus
和相机参数信息 _FocusData
:
final Map<GameStatus, _FocusData> _foci = {};
复制代码
CameraFocusingBehavior
监听着 GameState
的变化,所以可以在游戏状态变化时进行对应的处理。和新方法是 onNewState
回调中执行的 _zoomTo
方法:
这里 pinball
项目中封装了 CameraZoom
特效对动画缩放进行了封装,本质就是不断改变 zoom
值产生动画效果而已。其实 flame
本身应该提供对相机的动画缩放,已经动画结束的回调监听。
到这里,关于相机和视口就简单地介绍完毕。这个系列中,整个 Flame
的各个方面基本上都涵盖了,并且结合 Flutter
官方开源的 pinball
项目进行源码分析,或多或少对大家研究 Flutter
休闲游戏开发有所帮助。那第一季的 Flutter&Flame
游戏开发入门教程就到这里。
另外关于地图、flame_forge2d
等知识以后再说吧,是否开启第二季,会根据本系列的关注度、热度、或是 Flame
的发展综合考虑是否继续研究。目前看来,本系列的文章并没有太多人看,所以没有太大的动力去研究,我也不想投入太多的精力在游戏开发中。所以如果本系列对你有所帮助,还望多多点赞支持,后会有期 ~
@张风捷特烈 2022.06.27 未允禁转
我的 公众号: 编程之王
我的 掘金主页
: 张风捷特烈我的 B站主页
: 张风捷特烈我的 github 主页
: toly1994328