Day28. 虽然今年是2021,但我们要做2048(2)

昨天我们打好底了,有基本的容器,还有一些泛用的函式可以呼叫。今天来到实作第二天,让我们来看看今天的需求:

  • 基本容器宣告与画面初始
  • 依据滑动改变重力
  • 随机初始圆形
  • 滑行後生出新的图形但是短暂 Sleep
  • 滑行後唤醒全部的 Sleep 图形
  • 碰撞时判定合并
  • 合并计分
  • 自适应画面大小

划掉的是昨天完成的需求,粗体的是今天的内容。
今天要来处理核心的生成→碰撞→合并的机制!

今天的Demo
今天的Demo原始码
https://ithelp.ithome.com.tw/upload/images/20211013/20142057xy7ZpFD5ec.png

所以今天的需求依序有

  • 随机初始圆形
  • 滑行後生出新的图形但是短暂 Sleep
  • 滑行後唤醒全部的 Sleep 图形
  • 碰撞时判定合并

第一个随机初始圆形其实本来是要做转换方向後生成的初始球要随机生成 Level 1 或 Level 2的球,後来想想先留个空间,所以我们把 createBall 多丢一个 level,但先不改原本的初始与创建,同时当本来呼叫没有给 level 的情况下,我们会预设给一个 1 当输入。

function createBall(side,level)
{
    if(level == null) level = 1;
		.
		.
		.
		var ball = Bodies.circle(x, y, ballInfo.size, options = { label: level, render : {fillStyle: ballInfo.color}, isSleeping : true}, 80);
    Composite.add(engine.world, [ball]);
}

另外我们因为今天有了创建後的图形初始要是睡眠状态的,我们在这个创建球的函式加上 options : isSleeping : true,这样我们透过这个方法创建的初始球就会是睡眠静止的。

滑动後生成的部份我们先暂时设定成生成图形在滑动的相反方向,也就是上滑的话,就把球创建在下且静止,以此类推。因为我们昨天函式层级有拆乾净,这边的处理就很单纯,只要针对传入的方向再往里面传一层相反的方向就可以搞定。

// formObject.js
function generateBallAfterSwiped(side)
{
    switch(side)
    {
        case "top":
            createBall("down");
            return;
        case "down":
            createBall("top");
            return;
        case "left":
            createBall("right");
            return;
        case "right":
            createBall("left");
            return;
    }
}

滑行後唤醒我们这边用 forEach 的 syntax 来写。记得我们的 Composite 单元吗? engine.world 就是包含整个世界的组合体。组合体的内容有一个属性是 bodies ,会列出该组合体中无论层级的所有物体,我们遍历所有物体,目前我们没有指派 engine 来管理 isSleeping 状态,所以会控制 isSleeping 状态的只有我们自己,而我们有控制让 isSleeping 为 true 的时间点就只有当球被初始产生的时候。我们这边就透过一个 if 判断如果是睡眠的物体,我们就通通叫起来。

// swipedEvent.js
function swipeScreen(side)
{
		.
		.
		.
    engine.world.bodies.forEach(element => {
        if(element.isSleeping) element.isSleeping = false;
    });
		.
}

最後是今天的重点,我们要来做球碰撞後的判定与融合。

首先我们在 engine 上挂上碰撞判定事件,函式的话我们拆到另一个档案来处理。

我们的碰撞条件是两颗一样的球,碰撞後要做融合,那我们怎麽判断两颗球一样呢?我们上面 createCircle 的地方其实偷偷加了一个属性 label , label 挂上 level 的数值。所以我们只要判定两个碰撞对象 label 一致,我们就要处理碰撞融合。这边反过来写,当碰撞的两者 label 不同,我们就直接 return。

// main.js
.
Events.on(engine, "collisionStart", collisionTriggered);
.
// collisionHandle.js
function collisionTriggered(e)
{
    if(e.name == 'collisionStart' && e.pairs.length > 0)
    {
        if(e.pairs[0].bodyA.label != e.pairs[0].bodyB.label) return;
        ballCollision(e.pairs[0].bodyA.label, e.pairs[0].bodyA.id, e.pairs[0].bodyB.id);
    }
}

接着是我们的碰撞融合判断,我们会传入两个 body的 id,以及碰撞当下两个球体的等级,拿取 id 的目的是我们要从 world 的组合体中的 bodies 用 id 拿出对应的物体。

新的等级就是旧的等级 + 1,拿到新的等级後再去拿对应的颜色与大小,新的位置是碰撞两者的中点(计算我们抽到另一个 calculationHelper.js 来写) ─ 最後我们再用这些资讯创建新的球体。

前面透过 id 拿到的物体,除了要他们的位置以外,也顺便把他们从 world 移除,也就是在这个函式中我们完成了把碰撞的两者清除,用两者资讯创建一个新的球体加入世界这件事。

// formObject.js
function ballCollision(collisionLevel, bodyAId, bodyBId)
{
    var newLevel = parseInt(collisionLevel) + 1;
    var newBallInfo = getBallInfo(newLevel);
    var bodyA = engine.world.bodies.filter(x=>x.id == bodyAId)[0];
    var bodyB = engine.world.bodies.filter(x=>x.id == bodyBId)[0];
    var newPosition = getMiddlePlace(bodyA.position, bodyB.position);
    var ball = Bodies.circle(newPosition.x, newPosition.y, newBallInfo.size, options = { label: newLevel, render : {fillStyle: newBallInfo.color}}, 80);
    Composite.add(engine.world, [ball]);
    Composite.remove(engine.world, [bodyA,bodyB]);
}

到这里今天的内容其实已经完成,但笔者在测试的时候有发现昨天加的那个滑鼠按键在测试中不太实际,会按到手很酸,所以最後再加上一个常见的键盘按键控制,不过这段就没什麽特别的,一样就是呼叫滑动的函式跟带入方向。

// main.js
document.addEventListener('keydown', function(event) {
    switch (event.key) {
        case "ArrowLeft":
            swipeScreen("left");
            break;
        case "ArrowRight":
            swipeScreen("right");
            break;
        case "ArrowUp":
            swipeScreen("up");
            break;
        case "ArrowDown":
            swipeScreen("down");
            break;
    }
});

嘿!我们的 2048 已经有模有样了!让我们明天来帮它好好收尾,加上最後的一些内容,目前篇幅看起来是允许我们再加一些功能的个别实作,明天我们再来更新我们的需求清单,看看能不能多做些什麽让呈现上更完美。


<<:  Day26 Data Storage in iOS 02 - Keychain & Property list (Plists)

>>:  Day 28 - Spring Security (五) JwtAuthenticationProvider

[Day10]字符函数

字符函数,又分为大小写转换函数及字符处理函数。 大小写转换函数: 字符处理函数: 下篇会从日期单列函...

[SQL]取20天的平均

天啊.怎麽做啊? 同事用了一个聪明的作法, XD 这麽简单,怎麽没想到呢?脑袋卡卡! 我们先来做一下...

基於XML的标记来描述将呈现为HTML形式的用户界面元素是声明性编程范例

使用基於XML的标记来描述将呈现为HTML形式的用户界面元素是声明性编程范例。如果通过JavaScr...

Day3 风生水起,观元辰宫的火

在上一篇,除了水以外,这一篇就来讲火 五行中的火,除了"灶"以外,防火也是门学问...

[铁人赛 Day15] 如何分析 memoization 的成效呢?Profiler API

Why Profiler ? Profiler 可以用来测量 React app render 的次...