物理エンジンMatter.jsのサンプルゲーム


matter.js
 をダウンロードします。 https://brm.io/matter-js/ で Latest Buildを選ぶとGitHubに行くので、登録してサインインします。そうすると右の方のボタン「Clone or download」が選択可能になるので、「Download ZIP」を選択してZIPファイルをダウンロードします。解凍した「matter-js-master」の中のフォルダ「build」 のなかの「matter.min.js」だけ使います。
新規作成で新しいフォルダを作り、「testgame」など名前を付けて下さい。そこに「matter.min.js」をコピーして保存して下さい。メモ帳を開き、下記のソースリスト2つもコピーして「index.html」、「main.js」の名前を付けて文字コードUTF8でペーストして、作成したフォルダに保存して下さい。
「index.html」でサンプルゲームを実行できます。
地球を回る宇宙ごみの話題を見て、宇宙船がぐるぐる回るゲームが出来ないかと試してみました。ゲームは結局アイデアだと思います。
出来上がったものは下記からも実行できます。スマートフォンでも動作します。
https://gamemade.xyz/mattertest1/index.html

<!doctype html>
<html>
<head>
<meta charset="UTF-8"/>
<script src="matter.min.js"></script>
<style type="text/css">
* {margin: 0;padding: 0;}
html, body { width:100%; height:100%; }
.engine {
  position: absolute;
  left: 0;
  width: 700px;
  height: 1000px; }
  .engine canvas {
    width: 100%;
    height: 100%; }
</style>
</head>
<body ontouchmove="event.preventDefault()">
<div id="js-engine" class="engine"></div>
<script src="main.js"></script>
</body>
</html>

main.js

(function () {
    const Engine = Matter.Engine,//matter.js設定
        World = Matter.World,
        Constraint = Matter.Constraint;

    let container = document.getElementById('js-engine');
    let one_third = window.innerWidth / 3;//スマホの縦1/3座標
    let accel = 0;//自機加速
    let upaccel = 0;//自機上昇下降
    let enemy = [];//人工衛星
    let enemcon = [];//人工衛星とearthマルチボディ拘束
    let eneno = 30;//人工衛星の数

    var engine = Engine.create(container, {//matterエンジン700x1000ワイヤーフレームでない
        render: {
            options: { wireframes: false, width: 700, height: 1000, background: 'black' }//background: 'sky.jpg'
        }
    });
    //重力ゼロ
    engine.world.gravity.x = 0;
    engine.world.gravity.y = 0;

    let c1 = container;
    if (window.innerHeight < window.innerWidth * 10 / 7) {//パソコンとスマホで同じ大きさの画面にする
        h = window.innerHeight;
        w = window.innerHeight * 0.7;
    } else {
        w = window.innerWidth;
        h = window.innerWidth * 10 / 7;
    }
    c1.style.height = h + 'px';
    c1.style.width = w + 'px';
    divHeight = 1000 / h;
    divWidth = 700 / w;

    let player = Matter.Bodies.rectangle(350, 700, 40, 25, { friction: 0, frictionAir: 0.02, label: "player", render: { fillStyle: 'skyblue' } });//render: {sprite: {texture: 'player.png'}}
    let earth = Matter.Bodies.circle(350, 460, 90, { friction: 0, frictionAir: 0, label: "earth", render: { fillStyle: 'blue' } });
    let gps1 = Matter.Bodies.rectangle(350, 590, 40, 20, { friction: 0, frictionAir: 0, label: "gps", render: { fillStyle: 'maroon' } });
    let gps2 = Matter.Bodies.rectangle(350, 330, 40, 20, { friction: 0, frictionAir: 0, label: "gps", render: { fillStyle: 'brown' } });
    let gps3 = Matter.Bodies.rectangle(220, 460, 40, 20, { friction: 0, frictionAir: 0, label: "gps", render: { fillStyle: 'darkgreen' } });
    Matter.Body.setAngle(gps3, Math.PI / 2);//gps3を90度回転
    let gps4 = Matter.Bodies.rectangle(480, 460, 40, 20, { friction: 0, frictionAir: 0, label: "gps", render: { fillStyle: 'green' } });
    Matter.Body.setAngle(gps4, Math.PI / 2);//gps4を90度回転

    //マルチボディ拘束追加
    let constraint0 = Constraint.create({//earthを固定、画像あれば回転させるため
        pointA: { x: 350, y: 460 },
        bodyB: earth,
        render: { visible: false }
    });
    let constraint1 = Constraint.create({//player一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: player,
        pointB: { x: -20, y: 0 },
        stiffness: 0.01,
        damping: 0.6,//0.9
        render: { visible: false }
    });
    let constraint2 = Constraint.create({//playerもう一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: player,
        pointB: { x: 20, y: 0 },
        stiffness: 0.01,
        damping: 0.6,
        render: { visible: false }
    });
    let constraint3 = Constraint.create({//gps1一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps1,
        pointB: { x: -10, y: 0 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint4 = Constraint.create({//gps1もう一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps1,
        pointB: { x: 10, y: 0 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint5 = Constraint.create({//gps2一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps2,
        pointB: { x: -10, y: 0 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint6 = Constraint.create({//gps2もう一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps2,
        pointB: { x: 10, y: 0 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint7 = Constraint.create({//gps3一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps3,
        pointB: { x: 0, y: -10 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint8 = Constraint.create({//gps3もう一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps3,
        pointB: { x: 0, y: 10 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint9 = Constraint.create({//gps4一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps4,
        pointB: { x: 0, y: -10 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });
    let constraint10 = Constraint.create({//gps4もう一か所固定
        pointA: { x: 350, y: 460 },
        bodyB: gps4,
        pointB: { x: 0, y: 10 },
        stiffness: 0.01,
        damping: 0.9,
        render: { visible: false }
    });

    World.add(engine.world, [player, earth, gps1, gps2, gps3, gps4, constraint0, constraint1, constraint2, constraint3, constraint4, constraint5, constraint6, constraint7, constraint8, constraint9, constraint10]);
    Initialset();

    //衝突判定
    Matter.Events.on(engine, 'collisionStart', function (event) {
        pairs = event.pairs;
        for (i = 0; i < pairs.length; i++) {
            var pair = pairs[i];
            if (pair.bodyA.label === 'gps' || pair.bodyB.label === 'gps') {
                if (pair.bodyA == gps1 || pair.bodyB == gps1) {//gps1が何かに衝突したらマルチボディ拘束を切る
                    World.remove(engine.world, [constraint3]);
                    World.remove(engine.world, [constraint4]);
                }
                if (pair.bodyA == gps2 || pair.bodyB == gps2) {//gps2が何かに衝突したらマルチボディ拘束を切る
                    World.remove(engine.world, [constraint5]);
                    World.remove(engine.world, [constraint6]);
                }
                if (pair.bodyA == gps3 || pair.bodyB == gps3) {//gps3が何かに衝突したらマルチボディ拘束を切る
                    World.remove(engine.world, [constraint7]);
                    World.remove(engine.world, [constraint8]);
                }
                if (pair.bodyA == gps4 || pair.bodyB == gps4) {//gps4が何かに衝突したらマルチボディ拘束を切る
                    World.remove(engine.world, [constraint9]);
                    World.remove(engine.world, [constraint10]);
                }
            }
        }
    })

    //フレーム毎に実行
    Matter.Events.on(engine, 'beforeUpdate', function () {
        Matter.Body.applyForce(gps1, { x: gps1.position.x, y: gps1.position.y }, { x: Math.cos(gps1.angle) * 0.000057, y: Math.sin(gps1.angle) * 0.000057 });//gps1に前進力を与える
        Matter.Body.applyForce(gps2, { x: gps2.position.x, y: gps2.position.y }, { x: -Math.cos(gps2.angle) * 0.000057, y: -Math.sin(gps2.angle) * 0.000057 });//gps2に後進力を与える
        Matter.Body.applyForce(gps3, { x: gps3.position.x, y: gps3.position.y }, { x: Math.cos(gps3.angle) * 0.000057, y: Math.sin(gps3.angle) * 0.000057 });//gps3に前進力を与える
        Matter.Body.applyForce(gps4, { x: gps4.position.x, y: gps4.position.y }, { x: -Math.cos(gps4.angle) * 0.000057, y: -Math.sin(gps4.angle) * 0.000057 });//gps4に後進力を与える
        centang = Math.atan2(player.position.y - 460, player.position.x - 350)//playerとearthの中心との角度
        Matter.Body.applyForce(player, { x: player.position.x, y: player.position.y }, { x: Math.cos(player.angle) * (0.0016 + accel) + Math.cos(centang) * upaccel, y: Math.sin(player.angle) * (0.0016 + accel) + Math.sin(centang) * upaccel });
        for (let i = 0; i < enemy.length; i++) {//衛星の数だけ繰り返し
            if (enemy[i] != undefined) {//削除された衛星でない
                let distance = Math.sqrt(Math.pow(enemy[i].position.x - 350, 2) + Math.pow(enemy[i].position.y - 460, 2));//earthの中心から衛星までの距離
                if (distance < 140 || distance > 430) {
                    World.remove(engine.world, [enemy[i], enemcon[i]]);//衛星がearthに近すぎるか遠すぎれば取り除いて削除
                    delete enemy[i];
                    eneno--;//衛星の数を一つ減らす
                }
            }
        }
        if (eneno == 0) {
            Matter.Body.setPosition(gps1,{x:350, y:590});
            Matter.Body.setPosition(gps2,{x:350, y:330});
            Matter.Body.setPosition(gps3,{x:220, y:460});
            Matter.Body.setAngle(gps3, Math.PI / 2);//gps3を90度回転
            Matter.Body.setPosition(gps4,{x:480, y:460});
            Matter.Body.setAngle(gps4, Math.PI / 2);//gps4を90度回転
            World.add(engine.world, [ constraint3, constraint4, constraint5, constraint6, constraint7, constraint8, constraint9, constraint10]);
            Initialset()
        }
    });

    //物理シュミレーションを実行
    Engine.run(engine);

    window.addEventListener('keydown', (e) => {//パソコンキー入力
        e.preventDefault();
        if (e.key == 'ArrowLeft') { upaccel = - 0.06 }
        if (e.key == 'ArrowRight') { upaccel = 0.06 }
        if (e.key == 'ArrowUp') { accel = 0.001 }
    });
    window.addEventListener('keyup', (e) => {
        e.preventDefault();
        if (e.key == 'ArrowLeft' || e.key == 'ArrowRight') { upaccel = 0 }
        if (e.key == 'ArrowUp') { accel = 0 }
    });
    window.addEventListener('touchstart', (e) => {//スマホ入力
        e.preventDefault();
        for (let i = 0; i < e.changedTouches.length; i++) {
            if (e.changedTouches[i].pageX > one_third && e.changedTouches[i].pageX < one_third * 2) { accel = 0.001 }
            if (e.changedTouches[i].pageX > one_third * 2) { upaccel = 0.06 }
            if (e.changedTouches[i].pageX < one_third) { upaccel = - 0.06 }
        }
    }, { passive: false });
    window.addEventListener('touchend', (e) => {
        e.preventDefault();
        for (let i = 0; i < e.changedTouches.length; i++) {
            if (e.changedTouches[i].pageX > one_third && e.changedTouches[i].pageX < one_third * 2) { accel = 0 } else { upaccel = 0 }
        }
    }, { passive: false });

    //人工衛星
    function Initialset() {
        enemy = [];//人工衛星
        enemcon = [];//人工衛星とearthマルチボディ拘束
        eneno = 30;//人工衛星の数

        for (var i = 0; i < 30; i++) {
            enemy[i] = Matter.Bodies.circle(350, 612 + Math.floor(Math.random() * (770 - 612)), 12,{restitution: 1,frictionAir: 0,collisionFilter: { group: -1 }});//{ group: -1 }衛星同士がぶつからない
            enemy[i].render.fillStyle = 'darkorange'//enemy[i].render.sprite.texture = 'space1.png'
            Matter.Body.setAngularVelocity(enemy[i], 0.01);//人工衛星を回転
            enemcon[i] = Constraint.create({
                pointA: { x: 350, y: 460 },
                bodyB: enemy[i],
                stiffness: 0.0001,//マルチボディ拘束で弱いバネの様に振動
                render: { visible: false }
            });
            World.add(engine.world, [enemy[i], enemcon[i]]);
            Matter.Body.applyForce(enemy[i], { x: enemy[i].position.x, y: enemy[i].position.y }, { x: (Math.random() * (4 + 1 - 1) + 1) * 0.001, y: 0 });//x軸方向にランダムに力を加える
        };
    }

    //タイトル
    let fontsize = Math.round(window.innerHeight / 4);
    container = document.createElement('div');
    document.body.appendChild(container);
    const info = document.createElement('div');
    info.style.position = 'absolute';
    info.style.top = '30%';
    info.style.width = '70%';
    info.style.color = "yellow";
    info.style.fontWeight = "bold";
    info.style.fontSize = String(fontsize) + "%";
    info.innerHTML = 'PC: [←][↑][→]<br>スマホ:画面タッチ[左][中][右]';
    container.appendChild(info);
    window.setTimeout(function () { info.innerHTML = ' ' }, 4000);//一定時間で表示を消す
})();