来到2048的最後一天!看看这麽多的删除线!虽然可能我们不一定能清光我们购物网站上的愿望清单,但是今天,让我们一起清空这里列的代办清单。
另外我们会加上这些元素,来让我们的整体游戏更完满。
我们稍微依照事件发生的顺序,排序一下需求,下面我们就照着这个顺序来处理:
(我把 Day29 的字缩小啦!让成品稍微好看一点,笔者的小私心XD )
这部份如果前几天的 Demo 各位有用手机试过就会知道,不滑不要紧,一滑,整个画面都滑动,所以我们要做些处理来解决这个问题。
首先是要依屏幕大小来决定 Canvas的长宽,我们的逻辑是如果小於 600 就依屏幕等宽,大於 600 则拿 600 当 Canvas 的长宽。
// main.js
.
.
const canvasWidth = screen.width < 600 ? screen.width : 600 ;
const canvasHeight = screen.width < 600 ? screen.width : 600 ;
接着是处理我们 html 档案的最外层,body,我们加上 style 中的 overflow: hidden 和 height: 100%让页面不显示卷动条,也不能卷动,这样就不影响我们滑动的体验了!
body{
background-color: #FEFAE0;
display: flex;
justify-content: center;
overflow: hidden;
height: 100%;
}
其他 css 跟 layout 排版笔者这边不多提,毕竟不是本文主体,对 style 有疑问可以直接 google, css 的大神会告诉你该怎麽好好写的。
文字附加指的是在球上追加文字显示,让玩家知道他现在每颗球是多少,这个对玩家的游戏体验是一个很大的帮助(笔者个人观感)。
尽管先前我们说过 Matter.js 没有内建提供这种在物体上追加绘制文字的功能,但 google 是个好东西,我们可以看到曾经有使用这在这个 Matter.js 的 repo issue 问过:
Is there anyway to render text in Matter.js?
答案是没有,但是集思广益下,下面有人提出了一个写法,是直接在 canvas 上即时依据传入内容用原生 canvas 的函式来做绘制文字,所以我们参考这个方法,就能写出下面这个绘制文字的函式:
// renderText.js
function renderText()
{
var ctx = document.getElementsByTagName("canvas")[0].getContext("2d");
if(!ctx) return;
for(var elementId in engine.world.bodies)
{
var targetBody = engine.world.bodies[elementId];
if(!Number(targetBody.label)) continue;
if(targetBody.render.text)
{
var fontsize = 20;
var fontfamily = "Arial";
var color = "#EEEEEE";
fontsize = targetBody.circleRadius;
ctx.textBaseline="middle";
ctx.textAlign="center";
ctx.fillStyle=color;
ctx.font = fontsize + 'px '+fontfamily;
ctx.fillText(targetBody.render.text,targetBody.position.x,targetBody.position.y);
}
}
}
函式里面做的事情是,当这个函式被呼叫,我们会去取用 ctx,也就是我们 canvas 的操作口。接着遍历世界中的所有物体,当 label 属性是数字(我们定义创建的圆形才会是数字,其他系统预设都会是文字),我们才会对该图形的 .text 属性进行取用,抓取该图形位置,并依照圆形的半径来决定文字大小,最後就是直接於 canvas 的对应位置上写字。
我们所有创建球体的部分都要因应这里加上下面这段:
// formObject.js
ball.render.text = Math.pow(2,newLevel);
来设定 render 中的 text 属性,数字我们就依 2048 的设计,照 2^n 的方式显示。
同时上面这个函式,我们要挂到一个事件触发来让它持续更新 canvas,这边笔者会用 afterUpdate 这个 Event,也就是处理完所有碰撞及引擎事件後。
// main.js
Events.on(engine, "afterUpdate", renderText);
说到游戏,常常少不了计分,我们也不能免俗,我们准备了两个栏位,一个是分数,一个是历史高分。
那我们的 2048 什麽时候加分呢?当我们球体发生碰撞要合并的时候,除了合并,我们也会加上对应新的球体的 2^level 的分数,也就是当 2 与 2 合并的时候,我们要拿到 2^2 = 4分。
这边就很单纯地用全域变数来处理,记得 html 里要加上对应的 element,可以在 demo 的 html 里搜寻 scoreDisplay 和 bestScoreDisplay 这两个 id。
//main.js
.
.
.
var score = 0;
var bestScore = localStorage.getItem('bestScore') ? localStorage.getItem('bestScore') : 0;
.
.
.
function updateScore(addAmount)
{
score += addAmount;
document.getElementById("scoreDisplay").innerHTML = score;
}
function updateBestScore(newBestScore)
{
bestScore = newBestScore;
localStorage.setItem('bestScore',bestScore);
document.getElementById("bestScoreDisplay").innerHTML = bestScore;
}
.
.
.
我们暂且把更新显示逻辑与跟新分数本身逻辑放一块,毕竟暂时没想到更新分数但不更新显示的例子。另外我们的最高分会用 localStorage 来存,让玩家关掉网页後重开仍是有高分纪录的,关於 localStorage ,请参考 MDN 文件。
而我们把更新最高分的逻辑放在结算时才一次更新,平常碰撞只会对分数做更动。
天下没有不散的筵席, 2048 也有它结束的时候,我们要帮游戏设定一个结束条件,让玩家在限制条件内更有紧迫感。原始的 2048 就是不能滑动的时候就算输了,所以我们比照定义属於我们的结束条件:每次滑动、生出新的图形时,计算是否图形占达方框一定比例。
这边用比例是因为笔者不想算得太仔细,用约略抓出一个面积,然後靠比例因子大小来人肉调整就好。
// calculationHelper.js
function getTotalAreaOfBalls()
{
var sum = 0;
engine.world.bodies.filter(x=>Number(x.label)).forEach(x => sum += x.area);
return sum;
}
function isAreaMeetFillGate()
{
var totalArea = Math.pow(canvasWidth-wallThickness*2,2);
var fillGateRate = 0.4;
return totalArea*fillGateRate < getTotalAreaOfBalls();
}
这边拿取球体面积的时候我们一个透过 engine.world.bodies ,辨识球体一样用上面讲到,只有 label 是数字的才是我们创建的球体,接着把他们的面积加起来。
同时容器面积我们用 canvas 的宽度来乘算(因为我们的墙壁长度就是等同 canvas 的宽度!),扣去我们做 offset的部分,最後就是简单的算方形面积。
判别就是把我们上面的叙述程序化,拿总面积乘上一个比例因子,若全部球体面积加起来超过这个阈值,则判定要触发结束条件。
// swipedEvent.js
function swipeScreen(side)
{
.
.
.
if(isAreaMeetFillGate())
{
triggerEnding();
}
}
function triggerEnding()
{
stopTheRunner();
if( score > bestScore)
{
updateBestScore(score);
alert("You got "+ score + " points and it is NEW HIGH SCORE!\nWell done!");
}
else{
alert("You got "+ score + " points!");
}
renderText();
}
结束就做几件事
最後的最後,就是当一场游戏完了,玩家又投了 10块钱想要续摊,那我们就要重制特定变数的状态与 Matter.js 部分相关内容,这个其实我们在 Day15 的弹珠台就做过了,这边笔者就借用一下自己写过的内容:
// main.js
function retry(){
event.preventDefault();
Engine.clear(engine);
Render.stop(render);
Runner.stop(runner);
render.canvas.remove();
render.canvas = null;
render.context = null;
render.textures = {};
init();
}
一模模一样样!因为笔者都有把初始化抽到一个函式,这边就做完必要的 reset 後直接呼叫初始函式让他重新呼叫就好了!
以上,就是我们今天提到,要帮我们的 2048 做的所有功能,看看我们的 Demo ,是不是已经有模有样了呢?
其实今天笔者还有做一些调整,如碰撞生成後再次确认新生成的物体碰撞是否需合并,算式、变数或函式移动, html 主体更动等等,如果想看做了那些改动的,可以去用档案比较的方式看看 Day28 和 Day29 差在哪,配合这两天的文章,应该能够理解笔者的改动,若有疑问,也可以提出来讨论。
那我们的第二个实作,圆形 2048 到这边就要结束了,也是我们 30 天的旅程即将画上尾声的信号,明天最後一天,让我们来回首这 30 天的路程,同时俯瞰 Matter.js 的模组我们过去怎麽走、实际上分类的分法,重新回想这 30 天走过的路程,如果要推人坑,也可以用我们明天的内容来帮助他人更快上手XD。
>>: Youtube Reports API 教学 - 告一个段落
经营你的职业生涯 人对於甚麽叫 理想职涯 都有一套自己的想法 在经营职涯时候 追求目标要有弹性 适...
useLocation 函数是当 URL 网址改变时useState()会返回一个新的包含有关目前U...
接续昨天 response headers 的部分 一样是看 Julia Evans 大大的可爱的图...
我相信您曾经有过这样的经历。当您遇到问题并想解决时,您总是会发现有太多因素会影响该问题。太多了,您根...
在程序语言中, 我们不只要掌握基本的语法, 还要去融会贯通, 掌握它的精随所在, 而物件导向正是C...