前置き
ゲームエンジン (フレームワーク) を使用せず、Electoron だけを使用して直接 Canvas オブジェクトにグラフィックを描画します。
要するに「Node.js + Chromium = Electron」なので、Chromium で動作する JavaScript なら Electron でも動作します。
1-1. npm と Node.js のバージョンを確認する。
1-2. ワーキングフォルダを作成し、npm で初期化する。入っていなかったら、Node.js をインストールしておいてください。
通常ユーザで PowerShell を実行 (≠管理者)
スタート (右クリック) から「Windows PowerShell(I)」を選択
>_ Windows PowerShell
Windows PowerShell Copyright (C) Microsoft Corporation. All rights reserved. 新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6 PS C:\Users\who> npm -v ⏎ 6.14.12 PS C:\Users\who> node -v ⏎ v14.16.1 PS C:\Users\who> exit ⏎
通常ユーザで PowerShell を実行 (≠管理者)2-2. Electron をインストールする。
スタート (右クリック) から「Windows PowerShell(I)」を選択
>_ Windows PowerShell
Windows PowerShell Copyright (C) Microsoft Corporation. All rights reserved. 新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6 PS C:\Users\who> mkdir example ⏎ PS C:\Users\who> cd .\example\ ⏎ PS C:\Users\who\example> npm init -y ⏎ Wrote to C:\Users\who\example\package.json: { "name": "example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } PS C:\Users\who\example>
2-3. Electron のバージョンを確認する。
>_ Windows PowerShell
PS C:\Users\who\example> npm install electron ⏎ > core-js@3.17.2 postinstall C:\Users\who\example\node_modules\core-js > node -e "try{require('./postinstall')}catch(e){}" Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library! The project needs your help! Please consider supporting of core-js: > https://opencollective.com/core-js > https://www.patreon.com/zloirock > https://paypal.me/zloirock > bitcoin: bc1qlea7544qtsmj2rayg0lthvza9fau63ux0fstcz Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -) > electron@14.0.0 postinstall C:\Users\who\example\node_modules\electron > node install.js npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN example@1.0.0 No description npm WARN example@1.0.0 No repository field. + electron@14.0.0 added 87 packages from 95 contributors and audited 87 packages in 32.454s 6 packages are looking for funding run `npm fund` for details found 0 vulnerabilities PS C:\Users\who\example>
>_ Windows PowerShell
PS C:\Users\who\example> .\node_modules\.bin\electron.cmd -v ⏎ v14.0.0 PS C:\Users\who\example>
3-1. index.js を作成する。
C:\Users\who\example\index.js (Canvas を定義して preload.js をロードする)3-2. preload.js を作成する。
// メインプロセス (index.js) "use strict" ; // プログラムのミスを見つけやすくするための宣言。 const path = require("path") ; // Electron モジュール (クラス) の用意。 const ele = require("electron") ; // イベントハンドラ 'ready' の定義。(Electron の初期化が完了したら呼ばれる) ele.app.on('ready', () => { // HTML を用意する。 let dynContent = "data:text/html; charset=utf-8," + [ "<html>", "<body id=body>", "<div align=center>", "<canvas id=screen width=320 height=200 style='border: solid black 1px;'/>", "</div>", "</body>", "</html>", ].join("") ; // ウィンドウを用意する。 let win = new ele.BrowserWindow({ width: 320 + 17*2, height: 200 + 96, webPreferences: { preload: path.join(__dirname, "preload.js") }, }) ; win.setMenu(null) ; // メニューバーを非表示にする。 // ウィンドウに HTML を表示する。 win.loadURL(dynContent) ; // ウィンドウが閉じたらインスタンスを解放する。 win.on('closed', () => { win = null ; }) ; }) ; // イベントハンドラ 'window-all-closed' の定義。(ウィンドウがすべて閉じたら呼ばれる) ele.app.on('window-all-closed', () => { if (process.platform == 'darwin') return ; // Apple は quit() の必要がない。 ele.app.quit() ; // アプリケーションを終了する。 }) ;
C:\Users\who\example\preload.js (ロジック本体)
// レンダラプロセス (preload.js) "use strict" ; const ball = { color: "silver", r: 3, x: 0, y: 0, ix: +1, iy: +1, dx: 0, dy: 0 } ; const racket = { color: "silver", w: 16, h: 3 } ; const cursor = { x: 0, y: 0 } ; let ctx = null ; let screen = { color: "black", w: null, h:null } ; let before = { x: -1, y: -1 } ; let stroke = [] ; // 玉の行動を決める。 function ball_action() { // 画面の壁に当たったら、方向を反転する。 if (ball.x < 0) { ball.x = 0 ; ball.dx *= -1 ; } if (ball.x >= screen.w) { ball.x = screen.w ; ball.dx *= -1 ; } if (ball.y < 0) { ball.y = 0 ; ball.dy *= -1 ; } if (ball.y >= screen.h) { ball.y = screen.h ; ball.dy *= -1 ; } // ラケットに当たったかどうかの判定。 if (cursor.y == ball.y) { if (cursor.x <= ball.x && ball.x <= cursor.x + racket.w) { // ラケットに当たった if (ball.dx > 0) { if (ball.x <= cursor.x + racket.w / 3) { // 左から入って左端に当たった ball.dx *= -1 ; // 反対側へ } else if (cursor.x + (racket.w / 3) * 2 <= ball.x) { // 左から入って右端に当たった ball.dx = Math.sign(ball.dx) * 2 ; // チョップ } else { // 玉の移動量を戻す。(X ベクトルの正負は変更しない) ball.dx = Math.min(Math.abs(ball.ix),Math.abs(ball.dx)) * Math.sign(ball.dx) ; } } else { if (ball.x <= cursor.x + racket.w / 3) { // 右から入って左端に当たった ball.dx = Math.sign(ball.dx) * 2 ; // チョップ } else if (cursor.x + (racket.w / 3) * 2 <= ball.x) { // 右から入って右端に当たった ball.dx *= -1 ; // 反対側へ } else { // 玉の移動量を戻す。(X ベクトルの正負は変更しない) ball.dx = Math.min(Math.abs(ball.ix),Math.abs(ball.dx)) * Math.sign(ball.dx) ; } } ball.dy *= -1 ; // ラケットのどこかに当たったので、dy の符号を反転する。 } } // 玉を移動する。 ball.x += ball.dx ; ball.y += ball.dy ; } // 玉を描画する。 function ball_draw(ctx) { ctx.fillStyle = ball.color ; ctx.beginPath() ; ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2) ; ctx.fill() ; } // ラケットの行動を決める。 function racket_action() { // ラケットの移動を加速させる。 let delta = 0 ; let z = 1 ; while (stroke.length > 0) { delta += stroke.shift().dx * z ; z *= 4 ; } // ラケットを移動する。 cursor.x += delta ; // ラケットが画面内に収まるように表示座標を補正する。 if (cursor.x <= 0) { cursor.x = 0 ; } if (cursor.x >= screen.w - racket.w) { cursor.x = screen.w - racket.w ; } } // ラケットを描画する。 function racket_draw(ctx) { ctx.fillStyle = racket.color ; ctx.fillRect(cursor.x, cursor.y, racket.w, racket.h) ; } // 画面全体を描画する。 function screen_draw() { racket_action() ; ball_action() ; ctx.fillStyle = screen.color ; ctx.fillRect(0,0, screen.w, screen.h) ; ball_draw(ctx) ; racket_draw(ctx) ; } // DOM (Document Object Model) のロード完了のイベントリスナを定義。 // DOM は preload の中で使う必要がある。 // document.addEventListener('DOMContentLoaded', evt => { // HTML で ID を screen として定義した Canvas オブジェクトを取得する。 const cvs = document.getElementById('screen') ; // 2D のコンテキストを取得する。 ctx = cvs.getContext("2d") ; // Canvas からサイズを取得する。 [ screen.w, screen.h ] = [ ctx.canvas.width, ctx.canvas.height ] ; // ラケットの高さ位置を計算する。 cursor.y = screen.h - racket.h * 2 ; // 玉の移動量を初期化する。 [ ball.dx, ball.dy ] = [ ball.ix, ball.iy ] ; screen_draw() ; // 初期画面を描画する。 let timerId = setInterval(screen_draw, 20) ; // 20ms (1/50 秒) ごとに画面を描画する。 }); // マウスカーソル移動のイベントリスナを定義。 // document.addEventListener('mousemove', evt => { let [ dx, dy ] = [ 0, 0 ] ; if (before.x != -1 && before.y != -1) { dx = evt.offsetX - before.x ; dy = evt.offsetY - before.y ; } stroke.push({ dx: dx, dy: dy }) ; before = { x: evt.offsetX, y: evt.offsetY } ; }) ;描画タイミングについて
TV アニメの表示速度は 30 フレーム/秒。
一般的な PC のディスプレイやビデオゲームは 60 フレーム/秒。
なので、本稿では間をとって 50 フレーム/秒 とした。(PC のディスプレイ速度より速くても意味がない)
本来は V-SYNC (ディスプレイの垂直同期信号) に合わせるべき (window.requestAnimationFrame) だが、ここではそれを利用していない。
Electron で実行するとマウスの左右動作にラケットが連動するので、玉を打ち返してみてください。
>_ Windows PowerShell
PS C:\Users\who\example> .\node_modules\.bin\electron.cmd . ⏎ PS C:\Users\who\example>
このページで動作中のデモ画面 (demo.js) は、上記プログラムとは構造が多少異なります。
- index.js が無い (Node.js でないため)
- ラケットの動作が自動
- 複数の Canvas に対応
- 打ち返す効果音を再生