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

卖了那麽多天关子的最後实作,今天就要揭晓我们的题目了:不知道大家有没有玩过前些年很火红的一款小游戏呢?如果没有,那请读者先到这个网址来线上玩一下。

这是一款叫做 2048 的游戏,游戏的目的是要尽可能的合出数值较大的方块。要怎麽合出大数值呢?当朝一个方向滑动的时候,所有的物体会往该方向移动到底,同时在这个移动过程,如果两个相同数值碰在一起,便会相加,变成一个更大的数值。数值基底是 2,也就是会 2 → 4 → 8 → ... → 2048 这样递增合成。

没错!我们就要用 Matter.js 做一款 2048!

但是用一个物理碰撞引擎做一个看起来这麽平面也不用碰撞的游戏,也太大材小用了,何况 2048 的仿作已经满街都是了,我们要稍微来做点改版 ─ 方形容器里的 2048!就像日本的摇彩球的感觉。

https://ithelp.ithome.com.tw/upload/images/20211012/20142057wKgXfXNPyw.jpg

我们可以滑动画面,操控容器里的重力,目的一样是把球状的物体越滚越大!

今天的Demo
今天的Demo原始码

https://ithelp.ithome.com.tw/upload/images/20211012/20142057O2plREbfvO.png

那跟弹珠台一样,我们先列一下我们的需求清单

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

清单是笔者目前想到先列的,随後可能会因每天的内容有所删改。

今天先是个起头,我们会做基本的 Setup,所以我们会做到的有:

  • 基本容器宣告与画面初始
  • 依据滑动改变重力

这次我们直接用资料夹来开专案,js 相关程序码也会依作用分档。

在配色上推荐大家一个网站: Coolors ,在对配色还没有太多想法的时候,可以用这个网站来随机颜色,并逐步固定颜色来帮你发想一套完整的颜色。

我们这次的配色也是用这个网站随机慢慢配出来的。

首先建构的初始化函式,我们一样包在一个 init 的函式中,方便我们做反覆初始化。

共用的模组与变数则直接在函式外部作为全域宣告。

// main.js
// module aliases
var Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Bodies = Matter.Bodies,
Composite = Matter.Composite,
Events = Matter.Events,
Plugins = Matter.Plugins;

var engine;
var render;
var runner;
const canvasWidth = 600;
const canvasHeight = 600;
const wallThickness = 30;

制作容器的时候,我们会顾虑到後面要做自适应,也就是画面大小是有可能调整的,但比例不变,所以我们这边宣告容器宣告长宽、位置的时候都用相对位置做宣告创建。同时因为是容器,我们会让他是静止不受力的,也就是 isStatic 为 true。

// main.js
var wallA = Bodies.rectangle(canvasWidth/4, canvasHeight/4, canvasWidth, wallThickness, { isStatic: true, angle : getRadiusByDegree(315), render:{fillStyle:"#BC6C25"}});
var wallB = Bodies.rectangle(canvasWidth*3/4, canvasHeight/4, canvasWidth, wallThickness, { isStatic: true, angle : getRadiusByDegree(45), render:{fillStyle:"#DDA15E"}});
var wallC = Bodies.rectangle(canvasWidth/4, canvasHeight*3/4, canvasWidth, wallThickness, { isStatic: true, angle : getRadiusByDegree(45), render:{fillStyle:"#283618"}});
var wallD = Bodies.rectangle(canvasWidth*3/4, canvasHeight*3/4, canvasWidth, wallThickness, { isStatic: true, angle : getRadiusByDegree(315), render:{fillStyle:"#606C38"}});

至於我们的主角球球,我们预计在这次的教学中仅以颜色和大小来表示不同数值,对,我们期望(隐藏的)数值越大的球,体积就要越大,每个大小,都有不同的颜色。

这边关系到笔者有点小懒惰,因为 matter.js 的物体没有提供直接附加文字显示的功能,要做到有数字内容显示会需要做 texture,我们先省工用颜色和大小来区别就好。

// formObject.js
function createBall(side)
{
    var x;
    var y;
    var offset = 20;
    switch(side)
    {
        case "top":
            x = canvasWidth / 2;
            y = wallThickness + offset;
            break;
        case "left":
            x = wallThickness + offset;
            y = canvasHeight / 2;
            break;
        case "right":
            x = canvasWidth - wallThickness - offset;
            y = canvasHeight / 2;
            break;
        case "down":
            x = canvasWidth / 2;
            y = canvasHeight - wallThickness - offset;
            break;
    }
    var ballInfo = getBallInfo(1);
    var ball = Bodies.circle(x, y, ballInfo.size, options = { render : {fillStyle: ballInfo.color}, isSleeping : true}, 80);
    Composite.add(engine.world, [ball]);
}

function getBallInfo(level)
{
    const ballColor = ["#D8F3DC","#B7E4C7","#95D5B2","#74C69D","#52B788","#40916C","#2D6A4F","#1B4332","#1B4332","#081C15"];
    var ballSzie = Math.sqrt(4*level)* 10;
    return {
        color : ballColor[level-1],
        size : ballSzie
    }
}

可以看到我们把方法拆成两个,一个是创建初始球用的,一个是拿来获取球类创建资讯的。这边暂且写死初始只会创建最低层级的球(以2048是创建 2/4,我们暂时先只创建最低层级),高层级的球会在後面透过碰撞来创建。把球的创建资讯拆出来就是为了让之後其他需要拿球创建资讯的地方可以共用。

我们定义了一个颜色阵列来做颜色渲染,由浅到深,同时球的大小我们用 (4 X 层级数)後开根号 * 10 来平衡,也就是从最小的层级 1 - 预计最高的层级 10,实际半径大小会是从 20 - 60,我们先暂时这样定义线性变大,如果後面不够明显我们再调整。

再来我们滑动侦测引用 这个 函式库,有别人造好的轮子我们就不重复造轮子啦。

这是笔者在找他人写法的时候找到的,使用上也很单纯,引入对应的 min.js 档案,在 document 的 加上对应的 EventListener 事件, Call Back 里面写上要做的事件。

// swipedEvent.js
document.addEventListener('swiped-up', function(e) {
    swipeScreen("up");
});
document.addEventListener('swiped-down', function(e) {
    swipeScreen("down");
});
document.addEventListener('swiped-left', function(e) {
    swipeScreen("left");
});
document.addEventListener('swiped-right', function(e) {
    swipeScreen("right");
});

function swipeScreen(side)
{
    gravityChange(side);   
}

我们把 swipeScreen 包成一个函式,接着在里面做我们所有画面滑动後要做的事情,以今天的目标来说就是要做重力方向变化的处理。

关於重力的变化,我们这样做:

// swipedEvent.js
function gravityChange(side)
{
    switch(side)
    {
        case "up":
            engine.gravity.x = 0;
            engine.gravity.y = -1;
            return;
        case "down":
            engine.gravity.x = 0;
            engine.gravity.y = 1;
            return;
        case "left":
            engine.gravity.x = -1;
            engine.gravity.y = 0;
            return;
        case "right":
            engine.gravity.x = 1;
            engine.gravity.y = 0;
            return;
    }

}

记得 Day22 那天我们看的内容吗?重力被安在 engine 底下,同时有 x,y 两个方向的施力,我们假设每次滑动都会因为滑动的方向让整体世界的重力变成往滑动方向。

为了让电脑版也能操作测试(滑动本身行为会在手机上被侦测到),我们加上了几个上下左右的按钮,模拟对应方向的滑动,可以试试让 runner 跑起来,按按按钮,看一下我们今天的实作内容。

明天,我们进一步进到游戏的核心部分,也就是处理碰撞後的融合、计分等等机制,还有滑动的时候球类的生成,敬请期待。


<<:  中阶魔法 - this 指向(二)

>>:  {Day30} 网路爬虫

Day08 - 【入门篇】OAuth 2.0 Playground

本系列文之後也会置於个人网站 这是入门篇的最後一天了,今天不会写什麽内容,但来带大家看个入门概念可...

Day 15 - Rancher 与 Infrastructure as Code

本文将於赛後同步刊登於笔者部落格 有兴趣学习更多 Kubernetes/DevOps/Linux 相...

[Day-14] while回圈

今天要来练习while回圈的部分 那这边就直接开始说明罗~ 程序码范例: while(条件判断){ ...

Day13-Webhook 实作(二)LINEBot 之 Echo bot

大家好~ 昨天我们已经将 LINEBot 安装完成啦~ 今天来做个 Echo bot 简单认识一下 ...

Day 11 - 除了写程序之外还要访谈厂商之体验

昨天的文章有提到计画案有部分的厂商是需要去挖掘的,也因此今天会来分享一个计画案出现前与厂商面谈的心得...