【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

10,516 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 30 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】【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 中提供了 moveTosnapTo 两个移动方法,分别表示动画移动到某点和立刻移动到某点。并且可以通过 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 的发展综合考虑是否继续研究。目前看来,本系列的文章并没有太多人看,所以没有太大的动力去研究,我也不想投入太多的精力在游戏开发中。所以如果本系列对你有所帮助,还望多多点赞支持,后会有期 ~