1. 背景
需要开发一个小镇游戏,包含建造建筑、升级建筑、建筑生产金币、收金币等功能。整体复杂度不是太高,主要是建筑的循环动画和地图上小车、风车等小元素的动画,所以考虑使用DOM CSS3动画来实现。
2. 开发问题解决
2.1 层级控制问题
小镇的舞台是用小程序提供的movable-area和movable-view组件来实现可以移动地图的效果。起初,为了方便地图内的元素一起移动,把背景地图和建筑层都放在一个movable-view中。实现的代码大概如下:
代码语言:html复制<movable-area>
<movable-view>
<!-- 背景层 -->
<view class="map-layer"></view>
<!-- 蒙层 -->
<view class="mask-layer"></view>
<!-- 建筑层 -->
<view class="building-layer">
<!-- 建筑 -->
</view>
</movable-view>
<!-- 按钮层 -->
<view class="button-layer"></view>
</movable-area>
但实际在进入建筑规划状态时,要求达到的效果是地图层在蒙层下,建筑层在蒙层上,同时,一部分操作的按钮在蒙层之下,规划“退出”按钮在蒙层之上(如下图)。
如果按上面的代码实现,蒙层放在movable-view里,那蒙层就受movable-view的层级的影响,无法灵活调整层级来只遮盖部分按钮,因此蒙层必须挪到movable-view外面,并且movable-view内只能保留一个元素层。
为方便控制层级,将地图背景层和蒙层到跟movable-view同级,这样通过z-index的控制就可以灵活调整蒙层的层级,层级调整代码如下:
代码语言:html复制<movable-area>
<!-- 背景层 -->
<view class="map-layer"></view>
<!-- 蒙层 -->
<view class="mask-layer"></view>
<movable-view>
<!-- 建筑层 -->
<view class="building-layer">
<!-- 建筑 -->
</view>
</movable-view>
<!-- 按钮层 -->
<view class="button-layer"></view>
</movable-area>
但是背景层脱离了movable-view后,就没法被拖拽移动了。这时可以借助wxs响应事件的特性,通过给movable-view绑定change事件,当movable-view被移动时,同时改变背景层的坐标,使背景层跟随移动。增加事件绑定代码如下:
代码语言:javascript复制// moveEvent.wxs
const moveHandler = (e, ownIns) => {
const map = ownIns.selectComponent('.map-layer');
map.setStyle({
left: `${e.detail.x}px`,
top: `${e.detail.y}px`,
});
};
module.exports = {
moveHandler,
}
代码语言:html复制<wxs module="event" src="moveEvent.wxs" />
<movable-area>
<!-- 背景层 -->
<view class="map-layer" style="position:absolute;left:{{x}}px;top:{{y}}px;"></view>
<!-- 蒙层 -->
<view class="mask-layer"></view>
<movable-view x="{{x}}" y="{{y}}" bindchange="{{event.moveHandler}}">
<!-- 建筑层 -->
<view class="building-layer">
<!-- 建筑 -->
</view>
</movable-view>
<!-- 按钮层 -->
<view class="button-layer"></view>
</movable-area>
通过改变left和top的方式在开发者工具上调试没有什么问题,但在iphone真机上调试试,拖动几下就出现界面闪烁,然后就小程序就闪退了。
分析原因,通过设置left和top的方式去改变位置会引起频繁的重排,在小程序的环境中频繁的触发重排就会导致小程序的闪退。因此需要使用不引起的重排的位移属性进行位置控制,那就是css3中的transform属性,通过transform的translate值来控制移动GPU进程会为其开启一个新的复合图层,不会影响默认复合图层(就是普通文档流),修改后的代码如下:
代码语言:javascript复制// moveEvent.wxs
const moveHandler = (e, ownIns) => {
const map = ownIns.selectComponent('.map-layer');
map.setStyle({
transform: `translate3d(${e.detail.x}px, ${e.detail.y}px, 0)`,
});
};
module.exports = {
moveHandler,
};
代码语言:html复制<wxs module="event" src="moveEvent.wxs" />
<movable-area>
<!-- 背景层 -->
<view class="map-layer" style="transform:translate3d({{x}}px,{{y}}px,0);"></view>
<!-- 蒙层 -->
<view class="mask-layer"></view>
<movable-view x="{{x}}" y="{{y}}" bindchange="{{event.moveHandler}}">
<!-- 建筑层 -->
<view class="building-layer">
<!-- 建筑 -->
</view>
</movable-view>
<!-- 按钮层 -->
<view class="button-layer"></view>
</movable-area>
至此,蒙层的层级控制问题得到解决,背景层和movable-view包含的建筑层也可以流畅的移动了。
2.2 动画实现问题
2.2.1 逐帧动画抖动
在移动端适配时,web端使用的是rem单位,小程序端使用rpx单位。小程序实现逐帧动画时使用rpx作为单位,在非标准375宽度的屏幕下,由于计算精度问题,逐帧动画的展示可能会出现抖动(如下图)。
为了避免这样的计算精度问题,我们在逐帧动画的样式中统一使用px作为单位,然后再按照当前设备屏幕宽度的对应的比例进行缩放,从而达到动画稳定效果。代码示例如下
代码语言:javascript复制const { screenWidth } = wx.getSystemInfoSync();
Page({
data: {
scaleVal: 0.5 * (screenWidth / 375),
}
})
代码语言:javascript复制<movable-view x="{{x}}" y="{{y}}" bindchange="{{event.moveHandler}}">
<!-- 建筑层 -->
<view class="building-layer" style="transform:scale({{scaleVal}});transform-origin:0 0 0;">
<!-- 建筑(内部CSS使用px做单位) -->
</view>
</movable-view>
使用px作为单位并按比例缩放父容器完美解决逐帧动画抖动问题。
2.2.2 可变的动画
当前版本小镇开放了5个建筑,每个建筑都有建造中、运行中、销毁中这3种逐帧动画的状态,示例如下:
每个建筑又有10个等级,总共有150套动画样式要写,如果按传统的动画实现写法,一套动画的css实现代码如下:
代码语言:css复制/* 建造营业中动画 */
.mall-lv10-open {
display: block;
width: 100%;
height: 100%;
animation: mall-lv10-open-animation 1s steps(24) infinite;
background-size: 10100rpx 314rpx;
background-image: url(https://680acd229c2c4c6db1594628946e6899.png);
}
@keyframes mall-lv10-open-animation {
0% {
background-position: 0 0;
}
100% {
background-position: -9696px 0;
}
}
如果按照如上写法,要写150套这样的代码,代码量是相当大的,如果有中间有修改查找起来也会特别麻烦;另外,建筑的形态和动画等希望是能通过后台系统配置后,通过接口下发给前端展示的,像上面这种hardcode代码的形式就无法实现灵活的配置。
一般的css样式可以通过标签上行内样式的方式,用js生成后注入,但是css3的动画@keyframes属性无法在行内样式中使用,只能在css文件或者<style>标签里面使用,而小程序是无法动态注入样式或代码的,所以我们唯一要解决的问题是在小程序内怎么动态的设置@keyframes。
通过分析,发现所有的建筑动画的@keyframes基本都是一样的,唯一变的是背景图的宽度,在逐帧动画里就是背景图的位移background-position,只要能实现动态改变@keyframes里的background-position的值就可以了。这里我们使用的是css变量的方式。
首先对json对每个建筑的不同等级、不同状态进行配置:
代码语言:json复制{
"mall": {
"lv1": {
"width": 364,
"height": 249,
"init": {
"url": "https://250e112b90e641c7bd46edae65ecf671.png",
"frame": 11,
"fillMode": "forwards"
},
"working": {
"url": "https://bb371286db4d434180b0874172bb9a6a.png",
"frame": 19,
"fillMode": "infinite"
},
"destory": {
"url": "https://78ba80096c8940c0b106ebb1bcc09075.png",
"frame": 11,
"fillMode": "forwards"
}
},
...
},
...
}
然后js通过解析json进行css样式生成,生成的样式中包含--bgWidth作为css变量传入到行内样式的style中:
代码语言:javascript复制export const getBuildingAnimationStyle = ({ url, width, height, frame, fillMode, duration }) => {
const step = frame - 1;
return [
`--bgWidth:-${width * step}rpx;`,
`animation:building-animation ${duration || (frame / 10)}s steps(${step}) ${fillMode || 'infinite'};`,
`background-size:${width * frame}rpx ${height}px;`,
`background-image:url(${url});`,
].join('');
};
css部分通过var(--bgWidth)来使用传入的动态变量:
代码语言:javascript复制@keyframes building-animation {
0% {
background-position: 0 0;
}
100% {
background-position: var(--bgWidth) 0;
}
}
通过css变量的方式就可以轻松实现了可变动画的功能,节省了上千行的代码量。
这里还有一个小坑,在通过js生成animation传入行内style中时,如果要动态切换动画(像惠聚小镇的建筑的就是由销毁中动画切换到建造中动画,然后再切换到运行中动画),必须先把整体样式属性置空一次,然后再进行动画设置,否则可能会出现动画错乱。
3. 结尾
本文只是先分享一些开发小镇游戏过程解决问题的小技巧,还有部分关于金币运动动画等实现以及性能问题优化相关的后续继续补充。