- 配列
- 配列.map()
- キャスト
- よく使う標準関数
- オブジェクト ←構造体
- オブジェクトの破棄
- よく使う記号(要素名の頭に付くやつ)CSSでも同様らしい
- HTMLの要素をクリアする方法
- JavaScriptからはデータベースにはアクセスしない
- 指定要素へ移動
- HTML要素取得、主要メソッド一覧
- document.addEventListener()
- getBoundingClientRect()
- const dist = Math.sqrt(dx * dx + dy * dy);
- fetch()
- Nonce
- .then()
- style.display = “block”;
- trim()
- Promiseとは
- setInterval() 自動で繰り返し実行
- canvas関係
- getBoundingClientRect()
- context.arc()
- 環境でコードを切り換える
- コードメモ
- ctx.moveTo()
- ctx.save(); ctx.restore();を使う場面
- コード共通化のコツ
- ボタンのクラス管理
- ボタンのまとめ方
- 背景を画面下基準で拡大表示しつつ、パララックス付きで無限横スクロール風に描くコード
- トンネリング対策
配列
|
1 2 3 4 |
const array1 = []; const fruits = ["Apple", "Orange", "Plum"]; // 文字列 const numbers = [1, 2, 3, 4, 5]; // 数値 const mixed = [1, "Hello", true]; // 異なる型も混在可能 |
配列の末尾のカンマ(Trailing comma):
末尾にカンマを付けても問題ありません。要素の追加・削除が容易になります。
|
1 |
const array2 = ["a", "b", "c",]; |
取得: 配列名[インデックス] (0から始まる)
|
1 |
console.log(fruits[0]); // "Apple" |
更新:
|
1 |
fruits[1] = "Lemon"; // "Orange"を"Lemon"に更新 |
追加(末尾): push() メソッド
|
1 |
fruits.push("Grape"); |
配列の長さ(要素数): length プロパティ
|
1 |
console.log(fruits.length); |
多次元配列
配列の中に配列を入れます。
|
1 2 3 4 5 |
const matrix = [ [1, 2], [3, 4] ]; console.log(matrix[0][1]); // 2 |
配列.map()
JavaScriptのArray.prototype.map()は、配列の各要素に対して指定した関数を実行し、その結果からなる新しい配列を生成するメソッドです。
主な効果と特徴
- データの変換: 元の配列の要素を加工(例:数値を2倍にする、オブジェクトから特定のプロパティだけ抽出するなど)して、新しい形のデータ群を作れます。
- イミュータブル(不変)性: 元の配列を書き換えず、新しい配列を返します。元のデータを保持したまま加工後のデータを使いたい場合に最適です。
- 一貫した配列長: 原則として、返される新しい配列の要素数は元の配列と同じになります。
基本的な書き方
|
1 2 3 4 5 6 |
const numbers = [1, 2, 3]; // 各要素を2倍にする const doubled = numbers.map(num => num * 2); console.log(doubled); // [2, 4, 6] console.log(numbers); // [1, 2, 3](元の配列は変わらない) |
注意点
- 戻り値が必要: コールバック関数内で
returnしないと、新配列の要素がすべてundefinedになります。 - 副作用目的には不向き: 単に各要素を処理(例:ログ出力やDOM操作)するだけで新しい配列が不要な場合は、
forEachを使うのが一般的です。 - オブジェクトの参照: 配列の中身がオブジェクトの場合、プロパティを直接書き換えると元の配列にも影響が及ぶ(シャローコピー)ため、必要に応じてオブジェクトをコピーして返す工夫が必要です
例:配列の各要素にオブジェクトを持たせる
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function setupHandButtons(runtime) { const ui = getUIArea(runtime); const hands = ["guu", "choki", "paa"]; const spacing = ui.width / 6; runtime.ui.handButtons = hands.map((hand, index) => { return { hand, x: spacing * (index + 1), y: ui.y + ui.height / 2, radius: 40 }; }); } |
map() 内で外の変数(スコープ)は使えるか
はい、問題なく使えます。
JavaScriptの関数(map の中に書いたアロー関数など)は、自分が定義された場所の外側にある変数にアクセスできる性質(クロージャといいます)を持っています。
今回のコードでも、以下の外部変数が正しく参照されています。
spacing:mapの外で計算された数値ui:getUIAreaで取得されたオブジェクト
補足:なぜ map が使われるのか?for ループでも同じことはできますが、map を使うと「元のリストをベースに、新しいリストを組み立てる」という意図が明確になり、コードがスッキリ読みやすくなるメリットがあります。
キャスト
明示的なキャスト(型変換)
標準的なグローバル関数やコンストラクタを使用して、値を変換します。
- 数値への変換
- 文字列への変換
- 真偽値への変換
暗黙的なキャスト(型強制)
演算子を使用する際、JavaScriptが自動的に型を変換します。
- 文字列結合:
10 + '円'→'10円'(数値が文字列に変換される) - 算術演算:
'10' - 5→5(文字列が数値に変換される) - 条件式:
if (1) { ... }(数値の1がtrueとみなされる)
TypeScriptにおけるキャスト
TypeScriptでは、実際の値の変換ではなく、コンパイラに対して「この型として扱ってほしい」と伝える型アサーション(Type Assertion)を「キャスト」と呼ぶことがあります。
asキーワード:const val = someValue as string;- アングルブラケット:
const val = <string>someValue;
Array.from()
厳密なプログラミング用語としては、
Array.from() は「キャスト(型変換)」ではなく、「新しい配列インスタンスの生成(変換・コピー)」に分類されます。
ただし、文脈によっては「配列へのキャスト」として紹介されることもあります。
1. なぜ「キャスト」ではないのか
多くの静的型付け言語におけるキャストは、データの型情報を変えるだけで実体は同じである場合が多いですが、Array.from() は「元のオブジェクトを元に、新しい Array(配列)をメモリ上に作成する」という明確な「生成」のプロセスを含みます。
- 動作の本質: 反復可能なオブジェクト(Set, Map, Stringなど)や配列風オブジェクトから、浅いコピー(シャローコピー)された新しい
Arrayを作成する。 - 第2引数の存在:
Array.from(obj, mapFn)のように、変換と同時に各要素を加工する機能も持っており、これは単なる型変換を超えた「ファクトリメソッド」としての役割です。
2. 「キャスト」として扱われるケース
JavaScriptは動的型付け言語であるため、広義の意味で「あるデータ構造を別の型(この場合は Array 型)に作り替える」ことを指して「配列へのキャスト」と呼ぶ開発者もいます。
比較:JavaScriptにおける「キャスト」的な操作
JavaScriptで一般的にキャスト(明示的な型変換)と呼ばれるのは、以下のようなプリミティブ型への変換です。
| 変換先 | 方法(キャスト的) | Array.from() との違い |
|---|---|---|
| 数値 | Number(val) | 既存の値を解釈して値を返す |
| 文字列 | String(val) | 既存の値を文字列表現にする |
| 配列 | Array.from(val) | 新しい配列オブジェクトをメモリに生成する |
まとめ
- 技術的な正解: 配列を生成する静的メソッド(ファクトリメソッド)です。
- 会話上の表現: 「イテラブルを配列にキャストする」と言っても意味は通じますが、内部ではコピーが発生している点に注意が必要です。
よく使う標準関数
Math.random()
主な特徴と効果
- 範囲:
0 <= x < 1(0.0 ~ 0.999…9)。 - 特性: ほぼ均一な分布(擬似乱数)。
- 用途:
|
1 2 3 4 |
// 0~9の整数をランダムに取得 const randomInt = Math.floor(Math.random() * 10); // 1~6のサイコロの目 const dice = Math.floor(Math.random() * 6) + 1; |
objects.length
配列の要素数の取得
オブジェクト ←構造体
JavaScriptでは各オブジェクトリテラル {} は完全に独立した新しいオブジェクトとして生成されます。
|
1 2 3 4 5 6 7 8 9 10 |
class MyClass { items = [{ value: 0, extra: 99 }]; // 初期値 } const obj = new MyClass(); const newItem = { value: 5 }; // extraは存在しない obj.items.push(newItem); console.log(obj.items[0].extra); // 99(初期値の要素) console.log(obj.items[1].extra); // undefined(新規作成の要素) |
オブジェクトをnewした後、配列に代入する感覚で使うとバグる。
push等を使う時は要素全部を初期化する。
オブジェクトリテラルによる表現(最もシンプル)
データをその場で定義する際に使用します。
|
1 2 3 4 5 6 7 8 |
const person = { name: "山田太郎", age: 25, city: "東京" }; // アクセス方法 console.log(person.name); // 山田太郎 |
クラスによる構造体定義(構造の再利用)
同じデータ構造を持つオブジェクトを複数作成する場合に使用します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class User { constructor(name, age, city) { this.name = name; this.age = age; this.city = city; } } // 構造体のインスタンス化 const user1 = new User("佐藤花子", 30, "仙台"); const user2 = new User("鈴木一郎", 22, "盛岡"); console.log(user1.name); // 佐藤花子 |
new について
C言語の free や C++ の delete のような操作は不要です。その理由は以下の通りです。
1. ガベージコレクション(GC)の仕組み
JavaScriptエンジン(V8など)には、ガベージコレクション という自動メモリ管理機能が備わっています。new で作成されたオブジェクトがどこからも参照されなくなった(使われなくなった)と判断されると、エンジンが自動的にそのメモリを回収します。
2. 「どこからも参照されない」状態とは?
例えば、関数の中で new User(...) をした場合、その関数が終了して変数 user1 がスコープから外れれば、自動的に回収対象になります。
もし明示的に「もうこのデータはいらない」とエンジンに伝えたい場合は、変数に nullを代入します。
|
1 2 3 |
let user1 = new User(1, "田中", 25); // ... 何かの処理 ... user1 = null; // これで元のオブジェクトはどこからも参照されなくなり、GCの対象になる |
注意点:メモリリーク
自動管理とはいえ、メモリリーク(メモリが解放されない現象)が起きるケースもあります。
- グローバル変数に大きなデータを入れっぱなしにする
- 解除し忘れたイベントリスナー
- 使わなくなった後も残り続けるクロージャ内の変数
基本的には「使い終わった変数を放置せず、適切にスコープを限定する」だけで十分安全に動作します。
主な特徴とアクセス方法
- プロパティアクセス: ドット記法(
object.property)を使用して値を取得または変更します。 - 構造: { キー: 値, キー: 値 } のペアでデータを保持します。
- ネスト: オブジェクトの中にさらにオブジェクトを保持することも可能です。
オブジェクトの破棄
オブジェクトを解放する主な方法
変数に null や undefined を代入する
変数に別の値を代入することで、元のオブジェクトへの参照を解除します。
|
1 2 3 |
let obj = new MyClass(); // 処理... obj = null; // 参照がなくなるため、GCの対象になる |
注意点
- 強参照を残さない: 他のオブジェクトのプロパティや配列の中に参照が残っていると、変数に
nullを入れても解放されません。 - イベントリスナー:
addEventListenerで登録した関数がオブジェクトを参照している場合、removeEventListener などで解除しないとメモリリークの原因になります。
よく使う記号(要素名の頭に付くやつ)CSSでも同様らしい
| 記号 | 意味 | 例 | 説明 |
|---|---|---|---|
# | ID名 | "#main" | id="main" の要素を取得 |
. | クラス名 | ".active" | class="active" の要素を取得 |
| (なし) | タグ名 | "div" | <div> 要素を取得 |
[ ] | 属性 | "[name='user']" | name="user" 属性を持つ要素を取得 |
ID(#)を使う時のルール
- 唯一無二: 本来、HTML内で同じID名は1回しか使えません。そのため、特定の1箇所を確実に操作したい時に最適です。
- 大文字・小文字: ID名はアルファベットの大文字・小文字を区別するので、HTML側と完全に一致させる必要があります。
HTMLの要素をクリアする方法
- 要素.innerHTML = “”; (簡単で速い)
- 要素.replaceChildren();(モダンで安全)
- while (要素.firstChild) 要素.removeChild(要素.firstChild);(明示的)
JavaScriptからはデータベースにはアクセスしない
WordPressでJavaScriptからデータベース(MySQL)へ安全に直接アクセスするには、
主にWordPress REST APIを利用します。JavaScript(fetch APIなど)を使って、JSON形式で投稿データなどを取得・表示できます。直接のSQL操作はセキュリティリスクが高いため避けるべきです。
JavaScriptでWordPressデータベースにアクセスする主な方法
- REST APIの利用(推奨)
- AJAX (admin-ajax.php) の利用
実装のステップ
- WordPressのREST APIでデータを確認する。
- JavaScriptの
fetch関数を使用してAPIにGETリクエストを送信する。 - 返ってきたJSONデータをページに描画する。
直接MySQLへ接続するのではなく、REST APIを経由してデータ操作を行うのが安全なアプローチです
指定要素へ移動
一般的には「アンカーリンク」か「JavaScriptでスクロール」を使います。
- アンカーリンク方式(もっとも簡単)
ボタンにhref="#target"を付けて、移動先にid="target"を付けます。 - JavaScript方式(スムーズスクロールなど)
クリック時にdocument.querySelector("#target").scrollIntoView({ behavior: "smooth" });を呼びます。
HTML要素取得、主要メソッド一覧
| メソッド | 説明 | 戻り値 |
|---|---|---|
| document.querySelector(‘selector’) | CSSセレクタで最初に見つかった要素を取得 | 単一の要素 (Element) |
| document.querySelectorAll(‘selector’) | CSSセレクタに合致するすべての要素を取得 | NodeList(配列風) |
| document.getElementById(‘id’) | 指定したid属性を持つ要素を取得 | 単一の要素 (Element) |
| document.getElementsByClassName(‘class’) | 指定したclass名を持つすべての要素を取得 | HTMLCollection (配列風) |
| document.getElementsByTagName(‘tag’) | 指定したタグ名(div, pなど)のすべての要素を取得 | HTMLCollection (配列風) |
| document.getElementsByName(‘name’) | 指定したname属性を持つ要素を取得 | NodeList(配列風) |
メソッドの主な用途と違い
querySelector()/querySelectorAll()(推奨):getElementById():getElementsBy...系:
その他、関連する主な操作メソッド
document.createElement('tagName'): 新しいHTML要素を生成する。document.forms: ページ内のフォーム(form)のコレクションを取得。document.getElementsByTagName('body')[0]:body要素を取得
使用例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// IDで取得 const elementById = document.getElementById('header'); // クラスで取得(複数) const elementsByClass = document.getElementsByClassName('item'); // クラスで最初の一つを取得(CSSスタイル) const firstItem = document.querySelector('.item'); // クラスで全取得(CSSスタイル) const allItems = document.querySelectorAll('.item'); // タグで全取得 const allDivs = document.getElementsByTagName('div'); |
document.addEventListener()
document.addEventListener("DOMContentLoaded", () => { ... }) は、HTMLの読み込みと解析(DOMツリーの構築)が完了した時点でJavaScriptを実行するイベントリスナーです。画像やCSSの読み込みを待たずに即座にスクリプトが作動するため、表示速度向上とDOM操作のエラー防止に効果的です。
主な効果と特徴:
- 高速な処理開始: 画面の構造(DOM)ができた瞬間にJSを実行でき、画像やCSSの全読み込みを待つ
window.onloadより早いです。 - 要素操作の安全: スクリプトが実行される前にHTMLが解析されるため、HTML要素(
document.getElementByIdなど)が見つからないというエラー(null)を防げます。 - 非同期実行: HTMLの読み込みをブロックせず、画像などの読み込みと平行して処理が進みます。
この記述は、DOMの準備完了後にスクリプトを実行する、現代的なWeb開発で最も推奨されるイベント処理です。
各引数の詳細
1. 第1引数:イベントの種類 (type)
実行のトリガーとなるイベント名を文字列で指定します(例: 'click', 'submit', 'keydown')。
2. 第2引数:リスナー関数 (listener)
イベントが発生した際に実行する関数を指定します。
- 関数名のみ、または無名関数(アロー関数含む)を渡します。
- この関数には、自動的に「イベントオブジェクト(
event)」が引数として渡されます
3. 第3引数:オプション設定 (options / useCapture)
挙動を細かく制御するための設定です。主に以下の2通りの指定方法があります
- 真偽値 (
true/false):useCapture(イベントキャプチャリング)を使用するかどうか。デフォルトはfalse(バブリング形式)です。 - オブジェクト形式: 複数の詳細設定をまとめて指定できます。
基本的な使い方
|
1 2 3 4 5 6 7 |
// 要素を取得 const button = document.querySelector('button'); // 引数: 1.イベント型, 2.実行する関数 button.addEventListener('click', function(event) { console.log('ボタンがクリックされました', event); }); |
主要なイベントの種類
- マウス:
'click','dblclick','mousedown','mouseup','mousemove','mouseover' - キーボード:
'keydown','keyup','keypress' - フォーム:
'submit','change','input' - その他:
'load','resize','scroll'
注意点
addEventListenerは、同じ要素に同じイベントの複数のリスナーを追加できます。removeEventListenerで解除する場合は、登録時と同じ関数インスタンスを指定する必要があります。
getBoundingClientRect()
JavaScriptの getBoundingClientRect() は、特定の要素が「今見えている画面(ビューポート)のどこに、どのくらいの大きさであるか」を教えてくれるメソッドです。
ざっくり一言でいうと
「画面の左上を (0, 0) としたとき、その要素がどこにあるか」を丸ごと取得できます。
取得できる主な情報
メソッドを実行すると、以下のプロパティを持つオブジェクト(DOMRect)が返ってきます。
top:画面の上端から、要素の上端までの距離bottom:画面の上端から、要素の下端までの距離left:画面の左端から、要素の左端までの距離right:画面の左端から、要素の右端までの距離width/height:要素自体の幅と高さ(ボーダーやパディングを含む)
使い方のイメージ
|
1 2 3 4 |
const rect = element.getBoundingClientRect(); console.log(rect.top); // 画面の一番上からどれくらい離れているか console.log(rect.width); // 要素の横幅 |
⚠️ 注意点:スクロールで値が変わる
このメソッドで取れる数値は「画面(ブラウザの表示領域)」が基準です。
そのため、ページを下にスクロールすると、要素自体の場所は変わっていなくても topの値はどんどん小さく(マイナスに)なっていきます。
- 画面基準(ビューポート相対)で位置を知りたいなら:そのまま使う。
- ページ全体の中での絶対位置を知りたいなら:
rect.top + window.scrollYのようにスクロール量を足して計算する必要があります。
スクロールに応じてふわっと表示させるアニメーションなどを作る際によく使われる、フロントエンド開発の必須ツールです。
const dist = Math.sqrt(dx * dx + dy * dy);
クリックイベントでよくあるコード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const Rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - Rect.left; const mouseY = e.clientY - Rect.top; runtime.ui.handButtons.forEach(button => { const dx = mouseX - button.x; const dy = mouseY - button.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist <= button.radius) { runtime.ui.selectedHand = button.hand; } }); |
そのコードにおける
Math.sqrt(dx * dx + dy * dy) は、「クリックした地点」と「ボタンの中心点」の間の直線距離を求めています。
これは幾何学の三平方の定理(ピタゴラスの定理)を応用したものです。
このコードの「効果」
この計算の結果(dist)をボタンの半径(button.radius)と比較することで、「マウスが円形のボタンの内側にあるかどうか」を判定(当たり判定)しています。
dist <= button.radius:ボタンの中にマウスがある(クリック成功)dist > button.radius:ボタンの外側
四角形の判定(width や height を使う方法)とは違い、どの方向から近づいても中心からの距離で判定できるため、円形のUIパーツにはこの計算が必須となります。
より計算負荷を軽くするために、あえて Math.sqrt を使わず if (dx * dx + dy * dy <= button.radius * button.radius) と記述する最適化手法もありますが、興味はありますか?
の手法は、プログラミングやゲーム開発で**「平方根の比較(Squared Distance Comparison)」**と呼ばれる定番の最適化テクニックです。
コンピュータにとって、「平方根(Math.sqrt)の計算」は非常にコスト(計算負荷)が高い処理だからです。
- 通常の判定:
距離 < 半径(平方根の計算が必要) - 最適化した判定:
距離の2乗 < 半径の2乗(掛け算だけで済む)
1回や2回の計算なら差は無視できますが、ボタンが大量にある場合や、毎フレーム(秒間60回以上)判定を行うアニメーション処理などでは、Math.sqrt を省くことでCPUの負担を減らすことができます。
書き換えの例
元のコードを以下のように書き換えます。
|
1 2 3 4 5 6 7 |
// 変更前(ルートを計算する) const dist = Math.sqrt(dx * dx + dy * dy); if (dist <= button.radius) { ... } // 変更後(ルートを計算せず、右辺を2乗する) const distSq = dx * dx + dy * dy; if (distSq <= button.radius * button.radius) { ... } |
どちらを使うべき?
Math.sqrtを使う場合: 実際の「○px」という距離の値そのものが、ログ出力や他の計算(UIの拡大率など)で必要なとき。- 使わない場合: 単に「範囲内かどうか(当たり判定)」をチェックするだけのとき。
最近のPCやスマホは高性能なので、数個のボタン判定であればどちらでも動作に差はありませんが、「無駄な計算を省く」というエンジニアらしい綺麗な書き方として好まれます。
fetch()
fetchは、Web APIなどから外部リソース(データ)を非同期に取得する、標準的なHTTPリクエスト関数です。fetch(url)でリクエストを送り、Promiseオブジェクトを返すため、then()またはasync/awaitを使用してデータを取り出します。JSONデータを取得する際はresponse.json()を併用するのが一般的です。
基本的な使い方の概要
|
1 2 3 4 5 |
// 基本的なGETリクエスト fetch('https://jsonplaceholder.typicode.com/posts/1') .then(response => response.json()) // JSONとして解析 .then(data => console.log(data)) // 取得したデータを処理 .catch(error => console.error('Error:', error)); // エラーハンドリング |
1. よく使われる実装例:async/await
可読性が高いため、現代のJavaScriptではasync/awaitが推奨されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function fetchData() { try { const response = await fetch('https://api.example.com/data'); // レスポンスのステータスを確認 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log(data); } catch (error) { console.error('Fetch error:', error); } } fetchData(); |
2. POSTリクエストの送り方
第2引数にメソッド、ヘッダー、ボディ(データ)を設定します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fetch('https://api.example.com/data', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: 'foo', body: 'bar', userId: 1, }), }) .then(response => response.json()) .then(data => console.log(data)); |
3. 主なポイント・注意点
- 非同期処理:
fetchはPromiseを返すため、完了を待つ必要がある。 - Responseオブジェクト: 成功してもHTTPエラー(404や500など)の場合があるため、
response.okでチェックする。 - データの処理: レスポンスは
response.json()(JSON)のほか、response.text()(テキスト)、response.blob()(バイナリ)で解析可能。 - エラーハンドリング: ネットワーク障害などの物理的エラーは
.catch()で捕捉する。
4. Axios等との違い
fetchはブラウザ標準APIであり、ライブラリを追加する必要がない。一方、HTTPエラー(404など)でもPromiseが解決(成功)してしまうため、response.okによるステータス確認が必要。
ヘッダーについて
|
1 2 3 4 5 6 |
let response = await fetch("/〜〜/wp-json/〜〜/〜〜/〜〜", { method: "GET", headers: { "Content-Type": "application/json", // ←これ "X-WP-Nonce": wpApiSettings.nonce } |
Content-Type: application/json
これは
「このリクエストの本文(body)は JSON 形式ですよ」
とサーバー(ここでは **WordPress REST API)に伝えるヘッダです。
Content-Type ごとの挙動まとめ
| Content-Type | fetch body | PHP側の受け取り |
|---|---|---|
application/json | JSON.stringify() | $request->get_json_params() |
application/x-www-form-urlencoded | a=1&b=2 | $_POST |
multipart/form-data | FormData | $_POST + $_FILES |
| 無指定 | 不明 | ほぼ取れない |
Content-Type がある場合 / 無い場合の違い
✅ ある場合(正解ルート)
|
1 |
Content-Type: application/json |
サーバー側(WordPress REST)
- JSON として解析される
$request->get_json_params()が使える$req['challenge_id']が普通に取れる
|
1 |
$challenge_id = $request->get_json_params()['challenge_id']; |
❌ 無い場合(よくあるハマり)
|
1 |
Content-Type: (なし) |
起きること
- PHP側で body が空に見える
$req['challenge_id']が undefinednullが返ってバグる
→ 「送ってるのに受け取れない」現象
なぜ WordPress では JSON が推奨?
理由は3つ:
- SPA / アプリ風UIと相性が良い
- 配列・ネスト構造をそのまま送れる
- REST API標準
つまり今のガーチャリスの
「スマホアプリ風Webゲーム」設計と完全に合っています。
逆に JSON を使わなくていい場面
| ケース | 推奨 |
|---|---|
| ファイルアップロード | multipart/form-data |
| 旧来フォーム送信 | application/x-www-form-urlencoded |
| REST API | application/json |
Nonce
|
1 2 3 |
headers: { "X-WP-Nonce": wpApiSettings.nonce } |
これは
👉 「このリクエストは、今ログインしている正規ユーザーからです」
と WordPress に証明するための“通行証”です。
HTMLに隠したIDを改ざんされても、Nonceが無い攻撃は通りません。
Nonce とは何か?(超重要)
Nonce = ワンタイム系トークン
- ログインユーザーごとに発行
- 有効期限あり(約12〜24時間)
- JSからREST APIを叩くときの 本人確認
👉 **「CSRF対策」**が主目的です。
CSRF:
攻撃者が用意した悪意のあるページをユーザーに表示させている間、裏で他に開いているページでフォームに情報を書き込んだり、ボタンを押したり色々できる攻撃手段の事。
実装方法
PHP側:Nonce を JS に渡す
方法①(王道・必須)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
wp_enqueue_script( "スクリプト名", plugin_dir_url(__FILE__) . "〜〜.js", [], "1.0", true ); wp_localize_script( "↑で付けた同じスクリプト名", "wpApiSettings", [ "nonce" => wp_create_nonce("wp_rest") ] ); |
これで JS 側に
wpApiSettings.nonce
が生えます。
JS側:fetch に付ける
|
1 2 3 4 5 6 7 8 9 10 |
fetch("/wp-json/〜〜/〜〜/〜〜", { method: "POST", headers: { "Content-Type": "application/json", "X-WP-Nonce": wpApiSettings.nonce }, body: JSON.stringify({ challenge_id: 123 }) }); |
REST API 側:自動で検証される?
基本は「される」
permission_callback をちゃんと書けば
Nonceは WordPress側で自動検証されます。
よくある勘違い(重要)
❌ Nonce があれば何でも安全?
→ 違います
Nonceは
- 「誰が送ったか」
- 「同一サイトからか」
を保証するだけ。
.then()
.then()は、非同期処理(Promise)が成功した際に、その結果を受け取って後続の処理を実行するメソッドです。非同期の完了を待って順序立てて実行(チェーン)でき、引数には成功時と失敗時のコールバック関数を取ります。主にAPI取得やDB操作後の処理で使われます。
主な特徴と使い方
- Promiseの連鎖:
.then()は新しいPromiseを返すため、.then().then()のように数珠つなぎ(チェーン)にして処理を順に実行できます。 - 成功時の処理: 1つ目の引数にPromiseが成功 (
fulfilled) した時の関数を定義します。 - 結果の取得: 非同期処理が返した値(resolveされた値)を次の
.then()に引き継げます。
コード例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 非同期処理を定義 const fetchData = new Promise((resolve) => { setTimeout(() => resolve("データ取得完了"), 1000); }); // .then() の使用例 fetchData .then((result) => { console.log(result); // "データ取得完了" と表示 return "次の処理へ"; // 次の.thenへ値を渡す }) .then((message) => { console.log(message); // "次の処理へ" と表示 }); |
.then()の同義・類似手法
- async/await:
.then()の代わりに非同期処理を同期処理のように記述できる現代的な構文。 - .catch(): Promiseが失敗 (
rejected) した時の処理を定義する。 - .finally(): 成功・失敗に関係なく最終的に実行されるクリーンアップ処理。
style.display = “block”;
display: none;などで非表示になっていた要素を、幅いっぱいのブロックレベル要素として再表示させます。これにより、動的なポップアップ、ドロップダウンメニュー、要素の表示・非表示切り替えが実現できます。
主な効果と特徴:
- 要素の表示:
noneで消えていた要素が画面に現れます。 - ブロックレベル化: 要素は親要素の幅全体を占有し、前後に改行が入る形式になります(
<p>や<div>のような挙動)。 - レイアウトへの影響: 要素が表示されることで、他のコンテンツが押し下げられるなど、ページのレイアウトが再計算されます。
- 動的な制御: クリックやイベントに応じて、JavaScriptで即座に表示状態を変更する際によく利用されます。
例えば、document.getElementById("myElement").style.display = "block";と記述すると、IDが”myElement”の要素が表示されます。
trim()
文字列の先頭と末尾にある不要な空白(スペース、タブ、改行)を削除し、クリーニングされた新しい文字列を返す機能です。元の文字列は変更されず、ユーザー入力の不要な空白除去や、データ処理前のクレンジング(正規化)に必須のツールです。
trim() の主な効果と特徴
- 前後の空白除去: 文字列の両端にある空白文字(半角・全角スペース、タブ
\t、改行\n,\rなど)を削除。 - 中間の空白は保持: 単語間にあるスペースは削除せず、文字列の両端のみを処理。
- 新しい文字列を返却: 元の文字列は変更しないため、不変性(イミュータビリティ)を保ちながら安全にデータを整形。
- データクレンジング: ユーザー入力や外部データにおける不要な余白を消去し、エラーの可能性を低下。
|
1 2 |
let text = " Hello World! "; console.log(text.trim()); // "Hello World!" (前後が詰まる) |
関連・姉妹メソッド
- trimStart()()
: 先頭の空白のみを削除。 - trimEnd()(): 末尾の空白のみを削除。
注意点
- 文字列内(途中)のスペースは除去されません。
- 古いブラウザ(IEなど)では全角スペースの除去に対応していない場合があります。
Promiseとは
JavaScriptにおける
Promise(プロミス)は、一言で言えば「非同期処理の結果を後で受け取るための約束(器)」です。
ネットワーク通信やタイマー処理のように、結果が出るまで時間がかかる処理を、より分かりやすく効率的に記述するために使われます。
1. Promiseの3つの「状態」
Promiseは常に以下のいずれかの状態にあります。
- 待機 (Pending): 処理がまだ終わっておらず、結果を待っている初期状態。
- 成功 (Fulfilled/Resolved): 処理が無事に完了し、期待した値が得られた状態。
- 失敗 (Rejected): 処理中にエラーが発生し、失敗した状態。
2. 基本的な仕組みと使い方
Promiseオブジェクトを作成する際は、成功時に呼ぶ resolve と、失敗時に呼ぶ reject という2つの関数を引数に取ります。
.then(): 処理が成功した時の動きを指定します。.catch(): 処理が失敗した時の動きを指定します。.finally(): 成功・失敗に関わらず、最後に必ず実行したい処理を指定します。
3. なぜ使うのか?
かつてのJavaScriptでは「コールバック地獄」と呼ばれる、コードが複雑にネスト(入れ子)してしまう問題がありました。Promiseを使うことで、処理を .then() で数珠つなぎに(メソッドチェーン)書けるようになり、コードの可読性が劇的に向上しました。
さらに最近では、MDN Web Docsでも解説されている通り、Promiseをより直感的に書ける async / await 構文と組み合わせて使われるのが一般的です。
基本的な書き方
Promiseオブジェクトを作成する際は、resolve(成功時)と reject(失敗時)という2つの関数を引数に取ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const myPromise = new Promise((resolve, reject) => { const success = true; // 実際はここでAPI通信などの非同期処理を行う if (success) { resolve("成功しました!"); // 成功状態へ } else { reject("エラーが発生しました"); // 失敗状態へ } }); // 結果の受け取り myPromise .then((data) => console.log(data)) // 成功した時に実行 .catch((error) => console.error(error)) // 失敗した時に実行 .finally(() => console.log("完了")); // どちらの場合も最後に実行 |
setInterval() 自動で繰り返し実行
主な効果と活用シーン
- 定期的な更新: 時計の表示更新や、サーバーからの最新情報の自動取得。
- 動きの演出: アニメーションの制御や、画像スライドショーの自動切り替え。
- 時間計測: ストップウォッチやカウントダウンタイマーの作成。
注意点とリスク
- ブラウザの負荷: 実行間隔が短すぎたり、処理自体が重いと、画面がカクつくなど動作が重くなる原因になります。
- 停止が必要:
setInterval()は明示的に止めない限り動き続けます。不要になったら必ずWindow: clearInterval()を使って停止させてください。 - 実行タイミングのズレ: 前の処理が終わっていなくても次の実行が予約されるため、負荷が高い環境では想定通りの間隔で動かないことがあります。
正確な間隔を保ちたい場合や、負荷を抑えたい場合は再帰的なsetTimeoutを利用する手法も一般的です。
引数に関数を使用する場合【関数名()】の () は不要なのですか?
結論から申し上げますと、
setInterval の引数に関数を渡す場合、基本的には () は不要(書いてはいけない)です。
ここがJavaScriptを学び始めた時に最も混乱しやすいポイントの一つですので、理由を整理して解説します。
なぜ () をつけてはいけないのか?
JavaScriptにおいて、「関数名」と「関数名()」には大きな違いがあります。
- 関数名のみ(例:
sayHello) - 関数名() (例:
sayHello())
比較例
|
1 2 3 4 5 6 7 8 9 |
function greet() { console.log("こんにちは!"); } // ✅ 正しい書き方(関数を予約する) setInterval(greet, 1000); // ❌ 間違った書き方(今すぐ実行され、その結果が渡される) setInterval(greet(), 1000); |
例外:引数を渡したい場合はどうする?
もし実行したい関数に引数(パラメータ)を渡したい場合は、そのまま () を書くのではなく、以下のいずれかの方法を使います。
1. setInterval の第3引数以降を使う
setInterval には、実行する関数に渡す引数を後ろに並べることができる機能があります。
|
1 2 3 4 5 6 |
function greet(name) { console.log("こんにちは、" + name + "さん!"); } // 第3引数に "田中" を指定すると、greet("田中") として実行される setInterval(greet, 1000, "田中"); |
2. 無名関数(アロー関数)で包む
現在最も一般的な書き方です。
|
1 2 3 |
setInterval(() => { greet("田中"); }, 1000); |
まとめ
- 「あとで実行して」と頼む時は、
()は付けない。 - 「今すぐ実行して」という時だけ、
()を付ける。
setInterval は「あとで(定期的に)実行して」という命令なので、関数名だけを渡すのが正解です。
canvas関係
beginpath、clearrect 違い
1. 役割の決定的な違い
| メソッド | 役割 | 目的 |
|---|---|---|
beginPath() | パス(描画予定の線情報)の初期化 | 前に描いた図形の情報をリセットし、新しい図形を描き始めるため |
clearRect() | 描画済みピクセルの消去 | キャンバス上の特定の範囲(または全域)を透明にするため |
2. なぜ両方が必要なのか?
アニメーションを作る際などは、両方をセットで使うのが一般的です。
clearRect() が必要な理由
キャンバスは一度描くと「上書き」されます。古い絵を消さないと、動かしたはずの物体が残像のように伸びてしまいます。
beginPath() が必要な理由
Canvasは stroke() や fill() を呼ぶまで、それまでに指示された「線の通り道(パス)」をずっと覚えています。
もし beginPath() を呼ばないと、「1フレーム目に描いた線」と「2フレーム目に描いた線」が繋がったままになり、色を変えても全ての線が最新の色で塗り替えられるといった不具合が起きます。
3. 典型的な処理の流れ
|
1 2 3 4 5 6 7 8 9 |
// 1. 画面をきれいにする(見た目のリセット) ctx.clearRect(0, 0, canvas.width, canvas.height); // 2. パスを初期化する(データの記録をリセット) ctx.beginPath(); // 3. 新しい図形を描く ctx.arc(100, 100, 50, 0, Math.PI * 2); ctx.stroke(); |
ボタンの作成方法
JavaScriptのCanvas内には、HTMLのような標準の要素は存在しません。そのため、「図形として描画」し、「クリック位置の計算」でボタンの動作を再現する必要があります。
実装の3ステップ
- 図形の描画:
fillRectなどを使ってボタンの外観を描きます。 - イベントの監視:
canvas.addEventListener('click', ...)でクリックを検知します。 - 当たり判定(コリジョン): クリックされた座標(
event.offsetX,event.offsetY)が、描画した図形の範囲内(x, y, width, height)にあるかを条件式で判定します。
簡単な実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); // ボタンの定義 const btn = { x: 50, y: 50, w: 100, h: 40 }; // 1. 描画 ctx.fillStyle = 'blue'; ctx.fillRect(btn.x, btn.y, btn.w, btn.h); // 2. & 3. クリック判定 canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; if (mouseX >= btn.x && mouseX <= btn.x + btn.w && mouseY >= btn.y && mouseY <= btn.y + btn.h) { alert('ボタンが押されました!'); } }); |
効率的な方法
- ライブラリの利用: p5.js などのライブラリを使うと、ボタン作成用の専用関数(
createButton)が利用でき、複雑なUIも簡単に作成できます。 - HTML要素の重ね合わせ: ボタンの見た目やアクセシビリティを重視する場合、Canvasの上にCSSの
position: absoluteで本物の HTMLボタン要素 を重ねる手法が一般的です
getBoundingClientRect()
JavaScriptの getBoundingClientRect() メソッドは、要素のサイズと、ビューポート(ブラウザの表示領域)に対する表示位置を取得する効果があります。
このメソッドを呼び出すと、以下のプロパティを持つ DOMRect オブジェクト が返されます。
取得できる主なデータ
- 位置情報:
top,bottom,left,right(ビューポートの左上端を基準とした距離) - サイズ情報:
width,height(要素の幅と高さ。paddingやborderも含む) - 座標情報:
x,y(left,topと同等)
主な活用シーン
- スクロールアニメーション: 要素が画面内に入ったタイミングを検知して、フェードインなどのエフェクトを開始させる。
- 追従するUI: ボタンをクリックした際に、そのすぐ隣にポップアップやメニューを正確に配置する。
- スムーススクロール: 特定の要素までページをスクロールさせる際、その要素の正確な位置を計算する
注意点
- 基準点: 返される値は「ページ全体」の左上からではなく、あくまで「今見えている画面(ビューポート)」の左上からの距離です。
- スクロールの影響: 画面をスクロールすると、同じ要素でも
topやleftの値は変化します。 - 絶対座標の取得: ページ最上部からの絶対的な位置を知りたい場合は、
window.scrollY(スクロール量)を足す必要があります。
context.arc()
JavaScriptのcontext.arc()メソッドは、円弧(または円)を作成するためのパスを追加するメソッドです。
引数の構成は以下の通りです:
|
1 |
context.arc(x, y, radius, startAngle, endAngle, anticlockwise); |
引数の詳細
- x: 円の中心となるX座標。
- y: 円の中心となるY座標。
- radius: 円の半径。負の値を指定するとエラー(IndexSizeError)になります。
- startAngle: 円弧の開始角度(ラジアン単位)。右方向を0度とします。
- endAngle: 円弧の終了角度(ラジアン単位)。
- anticlockwise (省略可能): 描画する方向を指定する真偽値。
補足
- ラジアン計算: 角度(度)をラジアンに変換するには
(Math.PI / 180) * 角度という式を使用します。 - 円の描画: 開始角度を
0、終了角度をMath.PI * 2に設定することで正円を描画できます。 - 描画の実行: このメソッド自体は「パス」を作成するだけで、実際に画面に表示するには MDNの解説にあるように
stroke()(線)やfill()(塗りつぶし)を呼び出す必要があります。
環境でコードを切り換える
コードメモ
クリックイベントで的当て判定コード
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// ============================= // クリックイベントでターゲットがヒットしたかを判定し、スコアとコンボを更新します。ターゲットがヒットしなかった場合はゲームオーバーになります。 // ============================= async function handleClick(e, runtime, gameOptionState, userState) { if (!runtime.target) return; // キャンバス内のクリック位置を取得するために、イベントオブジェクトからクライアント座標を取得し、 // キャンバスの位置を考慮して正確なクリック位置を計算します。次に、クリック位置とターゲットの中心との距離を計算し、 // その距離がターゲットの半径以内であればヒットと判定します。 const rect = runtime.canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const dx = x - runtime.target.x; const dy = y - runtime.target.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance <= runtime.target.radius) { const reactionTime = (performance.now() - runtime.spawnTime) / 1000; let points = 300 - reactionTime * 200; // point if (distance <= 8) { // クリック位置がターゲットの中心から8px以内の場合は、さらにボーナスポイントを加算します。 points += 100; } points = Math.max(0, Math.floor(points)); gameOptionState.score += points; gameOptionState.comboCount++; document.getElementById("comboDisplay").textContent = "Combo: " + gameOptionState.comboCount; document.getElementById("scoreDisplay").textContent = "Score: " + gameOptionState.score; runtime.target = null; spawnTarget(runtime, gameOptionState); } else { await endGame(runtime, userState, gameOptionState); } } |
ヒットストップ
|
1 2 3 4 5 |
if (runtime.hitStop > 0) { runtime.hitStop--; requestAnimationFrame(() => animate(runtime, userState, gameOptionState)); return; } |
画面シェイク
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (runtime.shake > 0) { const dx = (Math.random() - 0.5) * runtime.shake; const dy = (Math.random() - 0.5) * runtime.shake; runtime.ctx.save(); runtime.ctx.translate(dx, dy); runtime.shake *= 0.9; } else { runtime.ctx.save(); } |
animate() 最後
|
1 |
runtime.ctx.restore(); |
クリック地点までの線の描写
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const ctx = runtime.ctx; const origin = getShotOrigin(runtime); ctx.strokeStyle = "rgba(255,255,255,0.8)"; ctx.lineWidth = 3; ctx.shadowColor = "rgba(255,255,255,0.8)"; ctx.shadowBlur = 10; ctx.beginPath(); ctx.moveTo(origin.x, origin.y); ctx.lineTo(runtime.aim.mouseX, runtime.aim.mouseY); ctx.stroke(); |
ctx.moveTo()
JavaScriptのCanvas APIにおけるctx.moveTo(x, y)メソッドは、描画の開始点(カレントパス)を指定した座標 (x, y) に移動させる効果があります。
このメソッド自体は、線を引いたり図形を描画したりせず、「ペンをキャンバスから離して、指定した位置へ移動させる」役割を果たします。
1. 描画開始位置の指定
新しいパス(線や図形)を描画し始める前に、その起点となる座標を指定するために使用します。通常、lineTo() や arc() と組み合わせて使用されます
2. パスの分断(ペンの持ち上げ)
すでにあるパスの描画中に moveTo() を呼び出すと、それまでの線を終了し、新しい描画位置まで線を描かずに(持ち上げて)移動できます。これにより、1つのパスの中で、離れた位置に複数の図形や線を描画できます
3. 具体的なコード例
|
1 2 3 4 5 6 7 8 9 10 11 12 |
const canvas = document.getElementById('myCanvas'); const ctx = canvas.getContext('2d'); ctx.beginPath(); ctx.moveTo(50, 50); // 点 (50, 50) に移動 ctx.lineTo(150, 50); // (50, 50) から (150, 50) へ線を引く // ここで moveTo を使って、線を描かずに別の場所に移動 ctx.moveTo(200, 50); ctx.lineTo(250, 100); // 別の線を描画 ctx.stroke(); // 描画を実行 |
lineTo() との違い
moveTo(x, y): 線を描かずに移動。lineTo(x, y): 現在の地点から(x, y)まで線を描きながら移動。
主に、図形を離して配置したい場合や、複雑な図形を描画する際の開始点を決めたい場合に必須となるメソッドです
ctx.save(); ctx.restore();を使う場面
JavaScriptのCanvas APIにおいて、
ctx.save() と ctx.restore() は、描画状態(スタイル、座標系、クリッピングなど)を一時的に保存し、後で復元するために使用します。
これらを使う主な場面は以下の通りです。
1. 座標変換(回転・拡大縮小・移動)を局所化する
ctx.rotate()、ctx.scale()、ctx.translate() を使用して描画を行う場合、その変換はキャンバス全体に影響します。特定の図形だけを動かしたい場合、描画後に変換を元に戻さないと、次の図形が意図しない位置・角度で描画されてしまいます。
- 場面: 複数の図形を異なる角度で回転・配置したい場合。
- 方法:
|
1 2 3 4 5 |
ctx.save(); // 現在の基準座標(0,0)を保存 ctx.translate(x, y); ctx.rotate(angle); ctx.fillRect(-25, -25, 50, 50); // 中心を基準に描画 ctx.restore(); // 変換前の座標系に戻す |
2. 描画スタイルを一時的に変更する
fillStyle(塗りつぶし色)や strokeStyle(線の色)、lineWidth(線の太さ)などを頻繁に変更する際、save/restoreを使うと、直前の設定を維持したまま、特定の図形だけスタイルを変更できます。
- 場面: グラフの棒ごとに色を変える、描画スタイルを元に戻す手間を省く。
- 方法:
|
1 2 3 4 5 6 7 |
ctx.fillStyle = 'black'; // 通常スタイル // ... 黒い図形を描画 ... ctx.save(); ctx.fillStyle = 'red'; // 一時的に赤にする ctx.fillRect(10, 10, 50, 50); ctx.restore(); // 赤い設定を破棄して黒に戻る |
3. クリッピング領域を設定する
ctx.clip() を使用して描画範囲を特定の図形の内側に制限する場合、そのままではその後の描画もクリッピングされたままになります。restore() でクリッピングを解除します。
4. アニメーションにおける描画の初期化
毎フレーム(requestAnimationFrameなど)で描画状態をクリーンな状態から開始するために、ループの最初で状態を保存し、最後に復元します。
まとめ:saveとrestoreのセット
ctx.save(): 現在の描画スタイル、変換行列、クリッピング領域をスタック(保存場所)にプッシュする。ctx.restore(): スタックから最後の保存状態をポップし、現在の描画コンテキストに適用する。
注意点: save() と restore() はペアで使う必要があります。save() だけを繰り返すとメモリを消費します
コード共通化のコツ
🎮 ミニゲームを量産するプロ構造
ゲームを増やすときに一番大事なのはこれです。
ゲームごとにJSを書かないこと
代わりに
ゲーム共通エンジン
↓
各ゲームは設定だけ
にします。
🧠 プロの設計イメージ
|
1 2 3 4 5 |
game-engine.js ← 共通処理 games/ clickGame.js dodgeGame.js memoryGame.js |
🎮 ① 共通ゲームエンジン
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class MiniGame { constructor(canvas, config){ this.canvas = canvas; this.ctx = canvas.getContext("2d"); this.config = config; this.score = 0; this.time = config.time || 30; this.running = false; } start(){ this.running = true; this.loop(); } loop(){ if(!this.running) return; this.update(); this.draw(); requestAnimationFrame(()=>this.loop()); } update(){ if(this.config.update){ this.config.update(this); } } draw(){ if(this.config.draw){ this.config.draw(this); } } } |
🎮 ② ミニゲームを「設定」で作る
クリックゲーム
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const clickGame = new MiniGame(canvas, { time:20, update(game){ // 更新処理 }, draw(game){ game.ctx.fillStyle = "black"; game.ctx.fillRect(0,0,800,600); game.ctx.fillStyle = "white"; game.ctx.font = "30px sans-serif"; game.ctx.fillText("SCORE:"+game.score,20,40); } }); |
🎮 ③ 別ゲームを追加
回避ゲーム
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const dodgeGame = new MiniGame(canvas, { time:30, update(game){ // 敵移動 }, draw(game){ game.ctx.fillStyle = "blue"; game.ctx.fillRect(0,0,800,600); } }); |
🎮 ④ ゲーム切り替え
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function startGame(type){ if(type === "click"){ clickGame.start(); } if(type === "dodge"){ dodgeGame.start(); } } |
🎮 ⑤ HTML
|
1 2 3 |
<button onclick="startGame('click')">クリックゲーム</button> <button onclick="startGame('dodge')">回避ゲーム</button> |
🚀 これの何がすごいか
ゲームを追加する時
JS1個書くだけ
|
1 2 3 4 |
games/ puzzleGame.js rhythmGame.js shootingGame.js |
ボタンのクラス管理
🎮 なぜ「ボタンシステム」が必要なのか
普通に作るとこうなります。
|
1 2 3 4 5 |
if(mouse.x > 300 && mouse.x < 500 && mouse.y > 200 && mouse.y < 260){ // スタートボタン } |
ボタンが増えると…
|
1 2 3 4 |
if(...) if(...) if(...) if(...) |
地獄になります。
なのでゲームでは
Buttonクラス
↓
UIManager
↓
Scene
この構造にします。
🧠 基本構造
Scene
↓
UIManager
↓
Button Button Button
🎮 ① Buttonクラス
まずボタンのクラスを作ります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
class Button{ constructor(x,y,w,h,text,onclick){ this.x = x; this.y = y; this.w = w; this.h = h; this.text = text; this.onclick = onclick; this.hover = false; } update(mouse){ this.hover = mouse.x > this.x && mouse.x < this.x + this.w && mouse.y > this.y && mouse.y < this.y + this.h; if(this.hover && mouse.clicked){ if(this.onclick){ this.onclick(); } } } draw(ctx){ ctx.fillStyle = this.hover ? "#ffd84d" : "#ffffff"; ctx.fillRect(this.x,this.y,this.w,this.h); ctx.strokeStyle="black"; ctx.strokeRect(this.x,this.y,this.w,this.h); ctx.fillStyle="black"; ctx.font="20px sans-serif"; ctx.textAlign="center"; ctx.textBaseline="middle"; ctx.fillText( this.text, this.x + this.w/2, this.y + this.h/2 ); } } |
🎮 ② UIManager
ボタンをまとめて管理します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class UIManager{ constructor(){ this.buttons = []; } add(button){ this.buttons.push(button); } update(mouse){ for(let b of this.buttons){ b.update(mouse); } } draw(ctx){ for(let b of this.buttons){ b.draw(ctx); } } } |
🎮 ③ Sceneで使う
例:タイトル画面
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class TitleScene extends Scene{ start(){ this.ui = new UIManager(); this.ui.add( new Button( 300, 300, 200, 60, "START", ()=>{ this.manager.change( new GameSelectScene(this.manager) ); } ) ); } update(){ this.ui.update(mouse); } draw(ctx){ ctx.fillStyle="black"; ctx.fillRect(0,0,800,600); ctx.fillStyle="white"; ctx.font="40px sans-serif"; ctx.fillText("MY GAME",300,200); this.ui.draw(ctx); } } |
🎮 ④ マウス入力
これもシンプルにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const mouse = { x:0, y:0, clicked:false }; canvas.addEventListener("mousemove",(e)=>{ const rect = canvas.getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top; }); canvas.addEventListener("mousedown",()=>{ mouse.clicked = true; }); |
ゲームループの最後で
|
1 |
mouse.clicked = false; |
🎮 結果
これでボタン追加は
|
1 2 3 |
this.ui.add(new Button(...)); this.ui.add(new Button(...)); this.ui.add(new Button(...)); |
だけ。
🔥 さらにプロっぽくする拡張
プロのゲームではボタンに
① アニメーション
|
1 2 |
scale hover animation |
② サウンド
|
1 2 |
hover sound click sound |
③ 画像ボタン
|
1 2 3 |
PLAY SHOP RANKING |
④ アイコンボタン
⚙️ 設定
🏆 ランキング
🎁 報酬
ボタンのまとめ方
普通に書くとJSはこうなります。
|
1 2 3 4 |
document.getElementById("register-btn").addEventListener(...) document.getElementById("revive-btn").addEventListener(...) document.getElementById("start-set-btn").addEventListener(...) document.getElementById("start-attack-btn").addEventListener(...) |
親要素1つだけでクリックを監視します。(janken-appというrootタグを持っている場合)
|
1 |
<div id="janken-app"> |
JSはこれだけ
|
1 2 3 |
document .getElementById("janken-app") .addEventListener("click", handleClick); |
クリック処理
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function handleClick(e){ const id = e.target.id; if(id === "register-btn"){ registerPlayer(); } if(id === "revive-btn"){ revivePlayer(); } if(id === "start-set-btn"){ startSet(); } if(id === "start-attack-btn"){ startAttack(); } } |
これだけで
全ボタン管理できます。
さらにゲーム向けにする(重要)
もっと良い方法があります。
data-action を使う
HTML
|
1 2 3 4 5 6 7 |
<button data-action="register">登録する</button> <button data-action="revive">復活する</button> <button data-action="startSet">カードをセットする</button> <button data-action="startAttack">カードに挑む</button> |
JS
|
1 2 3 4 5 6 7 8 9 10 11 |
document .getElementById("janken-app") .addEventListener("click", (e)=>{ const action = e.target.dataset.action; if(!action) return; actions[action](); }); |
アクション管理
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const actions = { register(){ registerPlayer(); }, revive(){ revivePlayer(); }, startSet(){ startSet(); }, startAttack(){ startAttack(); } }; |
メリット(かなり大きい)
① ボタン追加が超簡単
HTMLに書くだけ
|
1 2 3 |
<button data-action="showRanking"> ランキング </button> |
JS
|
1 2 3 |
actions.showRanking = function(){ openRanking(); } |
さらにゲームっぽくする(おすすめ)
手ボタン
|
1 2 3 |
<button class="hand-btn" data-hand="guu">✊</button> <button class="hand-btn" data-hand="choki">✌️</button> <button class="hand-btn" data-hand="paa">✋</button> |
JS
|
1 2 3 4 5 6 7 |
if(e.target.dataset.hand){ const hand = e.target.dataset.hand; selectHand(hand); } |
最終構造(ゲーム開発者スタイル)
JS
|
1 2 3 |
const app = document.getElementById("janken-app"); app.addEventListener("click", handleClick); |
処理
actions.js
api.js
ui.js
game.js
みたいに分割。
すると
コード量が1/3〜1/4になります。
背景を画面下基準で拡大表示しつつ、パララックス付きで無限横スクロール風に描くコード
|
1 2 3 4 5 6 7 8 9 10 11 |
draw(runtime) { const imgWidth = this.image.width; const imgHeight = this.image.height; const scale = runtime.height / imgHeight; const drawX = this.x + runtime.camera.x * (1 - 1 / (1 + this.speed * 0.5)); const drawY = runtime.height - imgHeight * scale; runtime.ctx.drawImage(this.image, drawX, drawY, imgWidth * scale, imgHeight * scale); runtime.ctx.drawImage(this.image, drawX + imgWidth * scale, drawY, imgWidth * scale, imgHeight * scale); } |
- 画像を画面高さに合わせて拡大縮小しています(縦フィット、横は比率維持)
drawYで画像の下端を画面下端に揃えています(地面が下に来る見え方)drawXはcamera.xをそのまま使わず係数付きで反映していて、レイヤーごとの視差(パララックス)を作ります- 同じ画像を2枚連続で描いているので、切れ目が見えにくい横ループ背景になります
補足すると、この式だと speed が大きいほどカメラ影響率が上がる設計です。
「遠景ほど遅く、前景ほど速く」を作る意図としては自然です。
トンネリング対策
線分と円の衝突判定
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function lineCircle(x1, y1, x2, y2, cx, cy, r) { const dx = x2 - x1; const dy = y2 - y1; const fx = x1 - cx; const fy = y1 - cy; const a = dx * dx + dy * dy; const b = 2 * (fx * dx + fy * dy); const c = fx * fx + fy * fy - r * r; let disc = b * b - 4 * a * c; return disc >= 0; } |
シンプル判定
|
1 2 3 4 5 6 7 8 9 10 11 |
function sweptCollision(player,obj){ const minX = Math.min(player.prevX, player.x); const maxX = Math.max(player.prevX, player.x); if(obj.x < minX-obj.r) return false; if(obj.x > maxX+obj.r) return false; return circleCollision(player,obj); } |
コメント