Day9. 一起动手做弹珠台!(1)

今天!!是令人兴奋的实作!!不再是单纯呼叫API文件的范例码,而是具备意义/情境的程序。之後只要出现这个标题,表示我们要用学到的知识分阶段来完成我们的弹珠台。

记得我们 Day3 的时候曾经开过一系列的需求吗?没关系,我相信快一个礼拜的现在,大家都忘了差不多了,我们这边帮大家 Recap 一下需求:

  • 建立方钉和颜色钉
  • 方钉和颜色钉会是固定的
  • 建立圆球
  • 圆球需要自由落体,受到重力影响
  • 圆球要能和方钉碰撞产生弹跳
  • 圆球和颜色钉碰撞的时候要侦测到碰撞并变色
  • 镜头移动,上带到下
  • 在开始前,画面要是静止的,直到我们发出Signal
  • 滑鼠和球体互动 (追加功能)

没错,粗体的就是我们今天要来完成的项目!好的,我们话不多说,直接开始吧!

今日的Demo
今日的Demo原始码

https://ithelp.ithome.com.tw/upload/images/20210924/20142057ONXdGD3iwo.png

今天会专注在世界的基础建造,也就是初始化。
一开始的共用模组跟一些常数我会统一宣告在外层全域。

// Global Settings / Variables
var Engine = Matter.Engine,
    Render = Matter.Render,
    Runner = Matter.Runner,
    Bodies = Matter.Bodies,
    Composite = Matter.Composite,
    Events = Matter.Events,
    BodyM = Matter.Body,
    Vertices = Matter.Vertices;

var engine;
var render;
var runner;
const canvasWidth = 400;
const canvasHeigh = 500;
const blockSize = 20;
const mainBallRadius = 15;
const minimumBlockGenerateX = 50;
const minimumBlockGenerateY = 100;
const blockSeparateX = canvasWidth - minimumBlockGenerateX - 50;
const blockSeparateY = canvasHeigh - minimumBlockGenerateY  - 100;
const minimumDistanceBetweenBlocks = 60;

再来过往我们直接做的初始化世界,我们用一个 init 的 function 来包住,这边笔者多设想了一个重置世界的 case,所以才会选择包起来。

function init()
{
    engine = Engine.create();
    render = Render.create({
        element: document.body,
        engine: engine,
        options:{
            wireframes:false,
            showIds:true,
            background: '#bfe9f5', //"#bfe9f5"
            width:canvasWidth,
            height:canvasHeigh
        }
    });
    formHiddenWall();
    formMainBall();
    formRandomBlocks(15);

    Render.run(render);
    runner = Runner.create();
}

~~好的!我们今天完工了!~~我们今天本来就是初始化世界就差不多完成需求了,所以其实所有要做的事情都写在 init 里了,我们带大家看一下我们怎麽初始世界的,在 init 的流程如果用白话来说会是:

创建引擎与渲染相关物件 → [[加入引擎中的世界] 加入左右两侧的墙防止球跑出去 → 加入主要的球体 → 加入随机产生的柱子] → 让渲染跑起来 → 预先宣告runner但不让它跑

完成以上後应该会看到如同我们今天一开始贴的图,会是静止的球在空中,当你按下 Run the runner 的按钮,球就会掉下来了!

我们从墙的创建开始看 - formHiddenWall()

function formHiddenWall()
{
    var wallLeft = Bodies.rectangle(-21, canvasHeigh/2, 40, canvasHeigh, { isStatic: true });
    var wallRight = Bodies.rectangle(canvasWidth+21, canvasHeigh/2, 40, canvasHeigh, { isStatic: true });
    Composite.add(engine.world, [wallLeft,wallRight]);
}

墙的创建算是最单纯的,基本上就是参考 canvas 的属性来决定墙的位置与长宽,位置要注意因为给的座标是墙的中心,所以会用这种取半的写法来让墙不要显示在画面内。

另外因为是一道不能移动的墙,我们在 options 中会设定 isStatic: true ,来表示它是一个静止的物体。

最後再加入 world 中就大功告成了!

第二个是我们的主角,球 - formMainBall()

function formMainBall()
{
    const mainBallInitX = canvasWidth/2;
    const mainBallInitY = 50;
    
    var mainBall = Bodies.circle(mainBallInitX, mainBallInitY, mainBallRadius, options = {
        restitution: 1,
        render:{
            fillStyle:"#FFFFFF"
        }
    }, 100);
    Composite.add(engine.world, [mainBall]);
}

球的话要注意的是球的初始位置,我们会放在画面的中上方,render 因为是会被看到的,我们设成白色的,另外,因为我们预期球是会发生弹跳的,这边我们给 options 中的 restitution 值,可以依据个人喜好决定弹性碰撞的程度,范例中我们用 1 来做完全弹性碰撞。

最後是比较麻烦的乱数生成方块 - formRandomBlocks(blockCount)

function formRandomBlocks(blockCount)
{
    var blockOptions ={
        render : {
            fillStyle : "#569cd8",
        },
        isStatic : true,
        angle : getRadiusByDegree(45)
    };
    
    var blockCoordinateList = [];
    
    for(var i=0; i<blockCount; i++)
    {
        var blockCoordinate = getRandomCoordinateForBlocks(blockCoordinateList);
        var block = Bodies.rectangle(blockCoordinate.x, blockCoordinate.y, blockSize, blockSize, blockOptions);
        blockCoordinateList.push(blockCoordinate);
        Composite.add(engine.world, [block]);
    }            
}

方块本身的建立是相对单纯的,麻烦的会是考量弹珠台的实作,球不能被卡在方块中间,所以我们会定义一个方块最小相距常数,在乱数产生的函式中( getRandomCoordinateForBlocks )会让方块彼此间的距离不要太近。另外在 options 比较多的时候,我们可以像这里一样把 options 抽成单独一个变数,会比较乾净也能和其他物体共用(如果有需要的话啦),我们这边设定的 options依序是蓝色填充、静止物体、旋转角度45度。

流程上来说会是

设定方块创建选项 → 宣告一个具有所有方块座标的阵列(用於检查距离) → [[回圈产生指定数量的方块] → 随机产生符合距离规范的方块座标 → 用 Bodies 中的方法以及参数创建方块 → 将方块加入世界中]

细部的函示笔者就不走进去细节了,大家可以先看看理解一下,或是尝试自己实作。
完成你的弹珠台後,可以按下 Run the runner 的按键,检视你今天的杰作!
嘿,球动起来了!它和那些方块碰碰撞撞!

我们最後加码一个,如果我们要 reset 的话会怎麽做?我们看到 reInit 这个函式:

function reInit()
{
    event.preventDefault();
    Engine.clear(engine);
    Render.stop(render);
    Runner.stop(runner);
    render.canvas.remove();
    render.canvas = null;
    render.context = null;
    render.textures = {};
    init();
}

这边依序是终止了发生中事件、清除引擎、停止渲染、停止跑动回圈,移除对应的canvas与纹理 ─ 最後,我们在执行一次我们一开始抽出来的初始化函式!

嘿!按下 Re Init的按钮,世界就这样又重新来过了!你可以一次次的让球重新掉落、重新生成碰撞方块了!

我们的弹珠台看起来已经有模有样了,不是吗?明天,让我们来熟悉其他的模组,一起为完成弹珠台继续努力!


<<:  [Day_10]资料储存容器(3) - 字典(dict)

>>:  【Day 15】CodePipeline x 老实的人别去大阪 x 老菜卜玩东京

缺乏计画的目标,只能叫做愿望。----目标设定篇(上)

缺乏计画的目标,只能叫做愿望。 A goal without a plan is just a wi...

劫持用户会话(hijack user sessions)

-VLAN组(来源:Cisco Press) VLAN是一种创建其广播域的网络分段和隔离机制。路由...

【17】训练到一半遇到 nan 吗? 梯度爆炸与梯度消失的测试实验

Colab连结 今天大家介绍 Gradient Exploding (梯度爆炸) 与 Gradien...

Day 15 - Order & Deal Event

本篇重点 Order & Deal Event 委托单失败OrderState内容 官方说明...

Day-03 认识Android模拟器

本次要来介绍如何建立Android Studio上的模拟器,以及有哪些优缺点。 首先我认为最大的优点...