某岛

… : "…アッカリ~ン . .. . " .. .
March 15, 2022

《もしもスマブラだった》中的镜头实现

镜头语言在设计游戏时非常重要,除了可以加强叙事,在一些玩法中也是 Gameplay 的重要组成成分。
(e.g. 在《狂父》的最后,aya 在倒地之后,父亲举起电锯的画面有 一个缩放镜头的特写
(Dungeon of the endless 中,需要缩放镜头来进行人员部署。。。

但是众所周知,RM2k3 里居然没有镜头对象,也没有像隔壁 Wolf RPG Maker 以及 GameCreator 里那样的缩放操作,使得需要缩放镜头的地方变得非常难以处理。。。
之前发现在场景切换的时候实际上是有 zoom-in,zoom-out 事件的,但是想进去魔改之后却还是失败了。。。

云风老师鼓励我们,分析开源项目的时候,应该假设自己是代码的作者,想象自己是怎么写出来的,其实 easyrpg 和 pico8 一样,都是非常标准的主循环结构,都是 …

while (true) {
  update();
  draw();
}

之后就去爬了一下官方的 Blog。。。在读博客的时候发现了好多此前闻所未闻的 RM2k3 游戏。。。在 这篇文章 中,还出现了一个吊炸天的作品。。。。。。もしもスマブラだったら。。。

这个玩意有多厉害我就不多说了。。。
https://yno.yumenikki.info/?game=smash。。。。

众所周知。。。任天堂明星大乱斗里。。由于有四位玩家同时作战,对镜头的控制相对于、侍魂、月华剑士等作品来说,要显得更加复杂。。。
这种在正常游戏引擎里要实现尚十分困难。。。这是怎么在 RM2k3 里实现的呢。。。。

怀着好奇的心情。。。我打开了游戏的档案。。。结果。。。。
https://t.me/algorithm_daily_of_minako/4418。。。。

也就是说整个游戏中 tilesmap 模块只做障碍判定用,而显示的部分,全部都是几个图片。。。
然后再用几个变量去计算出缩放距离。。。最后每一帧之后都通过一个 500 行的 if-else 去对图片进行缩放。。。。。

收下我的膝盖吧。。。

EasyRPG 中实现 zoom-in / zoom-out

Transition 中的 Zoom 变换

首先考察现有的 zoom-in/zoom-out 方法,首先是 Transition 里有一个,但是这个是一个动画,不太适合改成镜头操作。。。
最后底层会调用一个 Bitmap::StretchBlit 操作。

void Bitmap::StretchBlit(Rect const& dst_rect, Bitmap const& src, Rect const& src_rect, Opacity const& opacity, Bitmap::BlendMode blend_mode) {
	if (opacity.IsTransparent()) {
		return;
	}

	double zoom_x = (double)src_rect.width  / dst_rect.width;
	double zoom_y = (double)src_rect.height / dst_rect.height;

	Transform xform = Transform::Scale(zoom_x, zoom_y);

	pixman_image_set_transform(src.bitmap.get(), &xform.matrix);

	auto mask = CreateMask(opacity, src_rect, &xform);

	pixman_image_composite32(src.GetOperator(mask.get(), blend_mode),
							 src.bitmap.get(), mask.get(), bitmap.get(),
							 src_rect.x / zoom_x, src_rect.y / zoom_y,
							 0, 0,
							 dst_rect.x, dst_rect.y,
							 dst_rect.width, dst_rect.height);

	pixman_image_set_transform(src.bitmap.get(), nullptr);
}

我们看到最底层是 pixman_image_composite32搜一下这个函数,可以看出来这是一个 剪切 的操作。所以上面真正实现缩放的函数时剪切之后的两次 transform(),似乎应该去找更直接的方法。

Sprite 中的 Zoom 属性

我们发现 Sprite 类中果然是有 zoom 属性的,并且在很多地方都出现过。

void Sprite::BlitScreenIntern(Bitmap& dst, Bitmap const& draw_bitmap, Rect const& src_rect) const
{
	double zoom_x = zoom_x_effect;
	double zoom_y = zoom_y_effect;

	dst.EffectsBlit(x, y, ox, oy, draw_bitmap, src_rect,
		Opacity(opacity_top_effect, opacity_bottom_effect, bush_effect),
		zoom_x, zoom_y, angle_effect,
		waver_effect_depth, waver_effect_phase, static_cast<Bitmap::BlendMode>(blend_type_effect));
}
void Bitmap::EffectsBlit(int x, int y, int ox, int oy,
						 Bitmap const& src, Rect const& src_rect,
						 Opacity const& opacity,
						 double zoom_x, double zoom_y, double angle,
						 int waver_depth, double waver_phase, Bitmap::BlendMode blend_mode) {
	if (opacity.IsTransparent()) {
		return;
	}

	bool rotate = angle != 0.0;
	bool scale = zoom_x != 1.0 || zoom_y != 1.0;
	bool waver = waver_depth != 0;

	if (waver) {
		WaverBlit(x - ox * zoom_x, y - oy * zoom_y, zoom_x, zoom_y, src, src_rect,
				  waver_depth, waver_phase, opacity, blend_mode);
	}
	else if (rotate) {
		RotateZoomOpacityBlit(x, y, ox, oy, src, src_rect, angle, zoom_x, zoom_y, opacity, blend_mode);
	}
	else if (scale) {
		ZoomOpacityBlit(x, y, ox, oy, src, src_rect, zoom_x, zoom_y, opacity, blend_mode);
	}
	else {
		Blit(x - ox, y - oy, src, src_rect, opacity, blend_mode);
	}
}

进一步测试,很遗憾,上面的 zooim 看起来只是 cropping 。。。

这个看起来更像是我们需要的操作,例如,只要给 Sprite_Character 的元素都安排一个 zoom,就可以轻松的让玩家的图像和所有的地图事件都放大和缩小一个倍数了。
现在我们只需要对地图也进行同样的操作就可以了,但是麻烦的是,rm 中的地图实现是基于 tilemap 一块一块画出去的,直接在这里进行缩放的话势必需要修改不少代码。
最好我们可以先将整个地图绘制在一张位图上,再对这张位图进行变换操作看起来是最好的方法。

不过根据主循环函数,上面所有函数的 dst 对象只有一个,就是全局变量 DisplayUi->GetDisplaySurface()

void Player::Draw() {
	Graphics::Update();
	Graphics::Draw(*DisplayUi->GetDisplaySurface());
	DisplayUi->UpdateDisplay();
}