【Cocos Creator 3.8】KeyDoor(鍵付きドア)の実装:アタッチするだけで「所持している鍵を消費してドアを開ける」ギミックを実現する汎用スクリプト
このガイドでは、Cocos Creator 3.8.7 と TypeScript で「鍵付きドア」を実装します。
プレイヤーがドアに近づいて決定キー(例: Eキー)を押したとき、内部のインベントリに設定された「鍵アイテム」が必要数だけあれば消費してドアが開き、なければ開かない、という動作を行います。
特徴は以下の通りです。
- この
KeyDoorコンポーネント単体で完結し、他のカスタムスクリプトへの依存は一切ありません。 - 鍵アイテム・所持数・入力キー・ドアの開閉アニメーション(位置移動 or 回転)などをすべてインスペクタから調整できます。
- 「簡易インベントリ」をコンポーネント内部に持つため、サンプルや小規模ゲームならそのまま利用できますし、本格的なゲームではあとから外部インベントリと置き換える土台としても使えます。
コンポーネントの設計方針
1. 要件の整理
今回の KeyDoor コンポーネントが満たすべき要件を整理します。
- インスペクタで設定した「鍵アイテム名」と「必要数」を満たしているときのみドアを開ける。
- ドアを開ける際、鍵を必要数だけインベントリから消費する。
- ドアの開閉は、以下の2モードに対応する。
- 位置移動(スライドドア風)
- 回転(横開きドア風)
- プレイヤーがドアに「接触しているときのみ」決定キーで操作できるようにするため、2Dコライダー+トリガーを利用する。
- インベントリはこのコンポーネント内部に保持し、インスペクタから初期所持アイテムを設定できる。
- 外部の GameManager や Inventory シングルトンなどには一切依存しない。
2. 外部依存をなくすためのアプローチ
- インベントリは「単純なマップ(アイテム名 → 所持数)」として
KeyDoor内部に実装。 - インスペクタから「初期所持アイテム」を JSON 文字列で指定できるようにし、小規模なテストが即座にできるようにする。
- キー入力は Cocos Creator 標準の
systemEvent/inputを直接利用し、外部の入力管理スクリプトには依存しない。 - ドアの開閉アニメーションは
Nodeの位置または回転をtweenで補間する。 - 必要コンポーネント(
Collider2D/RigidBody2D)が付いていない場合は、onLoad内で警告ログを出す。
3. インスペクタで設定可能なプロパティ設計
以下のようなプロパティを定義します。
インベントリ関連
initialInventoryJson: string
– 形式: JSON 文字列(例:{"Key": 1, "GoldKey": 2})
– 説明: このドアが内部的に持つインベントリの初期状態。サンプルやテスト用として利用します。
– 空文字の場合は「所持アイテムなし」として扱います。
鍵アイテム設定
requiredItemName: string
– 説明: 鍵として消費するアイテム名。インベントリのキーと一致する必要があります。
– 例:"Key","GoldKey"requiredItemCount: number
– 説明: ドアを開けるために必要なアイテム数。
– 例: 1 に設定すると「鍵1個で開くドア」になります。
入力設定
useKeyCode: KeyCode
– 説明: ドアを開けるために押すキーボードキー。
– 例:KeyCode.KEY_E、KeyCode.SPACEなど。
ドア動作設定
doorMode: DoorMode
– 列挙型:NONE,MOVE,ROTATE
– 説明:NONE: 見た目のアニメーションは行わず、論理的に「開いた状態」になるだけ。MOVE: ドアを指定方向に移動させて開く。ROTATE: ドアを指定角度だけ回転させて開く。
openMoveOffset: Vec3
– 説明:doorMode = MOVEのとき、ドアをどの方向にどれだけ移動させるか。
– 例:(0, 200, 0)で上方向にスライドするドア。openRotateEuler: Vec3
– 説明:doorMode = ROTATEのとき、ドアをどの角度だけ回転させるか(オイラー角)。
– 例:(0, 90, 0)でヨー軸に90度回転。openDuration: number
– 説明: 開くアニメーションにかかる時間(秒)。
– 例: 0.3 〜 0.8 秒程度が扱いやすいです。canClose: boolean
– 説明: 一度開いたドアを再度キー入力で閉じられるかどうか。
– true: トグル(開閉を繰り返す)。
– false: 一度開いたら閉じない(多くのアクションゲームでの一般的なドア)。
コライダー関連
requireTrigger: boolean
– 説明: true の場合、「プレイヤーがこのドアのトリガー領域に入っているときだけキー入力を受け付ける」。
– テスト簡略化のために false にすると、「常にキー入力で開閉可能」になります(デバッグ用途)。debugLog: boolean
– 説明: true にすると、インベントリの状態や開閉結果をconsole.logに詳細出力します。
TypeScriptコードの実装
以下が完成した KeyDoor.ts の全コードです。
import {
_decorator,
Component,
Node,
Vec3,
tween,
input,
Input,
EventKeyboard,
KeyCode,
Collider2D,
IPhysics2DContact,
RigidBody2D,
log,
warn,
error,
} from 'cc';
const { ccclass, property } = _decorator;
enum DoorMode {
NONE = 0,
MOVE = 1,
ROTATE = 2,
}
/**
* KeyDoor
* - 内部に簡易インベントリを持つ「鍵付きドア」コンポーネント。
* - 指定したアイテム名と個数を所持している場合のみ、キー入力でドアを開ける。
* - 鍵は消費される。
*/
@ccclass('KeyDoor')
export class KeyDoor extends Component {
// ===== インベントリ設定 =====
@property({
tooltip: 'このドアが内部的に持つインベントリの初期状態をJSONで指定します。\n' +
'例: {"Key": 1, "GoldKey": 2}\n' +
'空文字の場合、所持アイテムなしになります。',
})
public initialInventoryJson: string = '{"Key": 1}';
// ===== 鍵アイテム設定 =====
@property({
tooltip: '鍵として消費するアイテム名。\n' +
'initialInventoryJson で指定したキーと一致させてください。',
})
public requiredItemName: string = 'Key';
@property({
tooltip: 'ドアを開けるために必要なアイテム数。\n' +
'例: 1 で「鍵1個で開くドア」になります。',
min: 1,
})
public requiredItemCount: number = 1;
// ===== 入力設定 =====
@property({
tooltip: 'ドアを開けるために押すキーボードキー。',
type: KeyCode,
})
public useKeyCode: KeyCode = KeyCode.KEY_E;
// ===== ドア動作設定 =====
@property({
tooltip: 'ドアの開閉アニメーション方法を選択します。\n' +
'NONE: 見た目は変えず論理的に開いた状態のみ。\n' +
'MOVE: 指定方向に移動して開く。\n' +
'ROTATE: 指定角度だけ回転して開く。',
type: DoorMode,
})
public doorMode: DoorMode = DoorMode.MOVE;
@property({
tooltip: 'doorMode = MOVE のとき、ドアをどの方向にどれだけ移動させるかを指定します。',
})
public openMoveOffset: Vec3 = new Vec3(0, 200, 0);
@property({
tooltip: 'doorMode = ROTATE のとき、ドアをどの角度だけ回転させるか(オイラー角)を指定します。',
})
public openRotateEuler: Vec3 = new Vec3(0, 90, 0);
@property({
tooltip: 'ドアの開くアニメーションにかかる時間(秒)です。',
min: 0,
})
public openDuration: number = 0.5;
@property({
tooltip: 'true の場合、一度開いたドアを再度キー入力で閉じることができます(トグル動作)。\n' +
'false の場合、一度開いたら閉じられません。',
})
public canClose: boolean = false;
// ===== コライダー関連 =====
@property({
tooltip: 'true の場合、「プレイヤーがこのドアのトリガー領域に入っているときのみ」キー入力を受け付けます。\n' +
'false の場合、常にキー入力で開閉可能(デバッグ用途)。',
})
public requireTrigger: boolean = true;
@property({
tooltip: 'true にすると、インベントリ状態や開閉結果をコンソールに詳細ログ出力します。',
})
public debugLog: boolean = true;
// ===== 内部状態 =====
private _isOpen: boolean = false;
private _isAnimating: boolean = false;
private _originalPosition: Vec3 = new Vec3();
private _originalEuler: Vec3 = new Vec3();
private _playerInside: boolean = false;
// 簡易インベントリ(アイテム名 → 所持数)
private _inventory: Map<string, number> = new Map();
// コライダー参照
private _collider2D: Collider2D | null = null;
private _rigidBody2D: RigidBody2D | null = null;
// ===== ライフサイクル =====
onLoad() {
// 初期Transformを保存
this._originalPosition = this.node.position.clone();
this._originalEuler = this.node.eulerAngles.clone();
// インベントリ初期化
this._parseInitialInventory();
// コライダー取得&イベント登録
this._setupCollider();
// キー入力イベント登録
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
if (this.debugLog) {
log('[KeyDoor] onLoad 完了 - node:', this.node.name);
}
}
onDestroy() {
// 入力イベント解除
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
// コライダーイベント解除
if (this._collider2D) {
this._collider2D.off('onTriggerEnter', this._onTriggerEnter, this);
this._collider2D.off('onTriggerExit', this._onTriggerExit, this);
this._collider2D.off('onBeginContact', this._onBeginContact, this);
this._collider2D.off('onEndContact', this._onEndContact, this);
}
}
// ===== インベントリ処理 =====
private _parseInitialInventory() {
this._inventory.clear();
if (!this.initialInventoryJson || this.initialInventoryJson.trim() === '') {
if (this.debugLog) {
log('[KeyDoor] initialInventoryJson が空のため、所持アイテムなしとして開始します。');
}
return;
}
try {
const obj = JSON.parse(this.initialInventoryJson) as Record<string, number>;
for (const key in obj) {
const count = Number(obj[key]);
if (!isNaN(count) && count > 0) {
this._inventory.set(key, count);
}
}
if (this.debugLog) {
log('[KeyDoor] インベントリ初期化完了:', this._dumpInventory());
}
} catch (e) {
error('[KeyDoor] initialInventoryJson のパースに失敗しました。JSON形式を確認してください。', e);
}
}
private _dumpInventory(): string {
const arr: string[] = [];
this._inventory.forEach((v, k) => {
arr.push(`${k}: ${v}`);
});
return `{ ${arr.join(', ')} }`;
}
private _hasRequiredItems(): boolean {
const current = this._inventory.get(this.requiredItemName) || 0;
return current >= this.requiredItemCount;
}
private _consumeRequiredItems(): boolean {
if (!this._hasRequiredItems()) {
return false;
}
const current = this._inventory.get(this.requiredItemName) || 0;
const next = current - this.requiredItemCount;
if (next > 0) {
this._inventory.set(this.requiredItemName, next);
} else {
this._inventory.delete(this.requiredItemName);
}
if (this.debugLog) {
log(`[KeyDoor] アイテム "${this.requiredItemName}" を ${this.requiredItemCount} 個消費しました。残り:`, this._dumpInventory());
}
return true;
}
// ===== コライダー関連 =====
private _setupCollider() {
this._collider2D = this.getComponent(Collider2D);
if (!this._collider2D) {
warn('[KeyDoor] このノードに Collider2D がアタッチされていません。' +
'requireTrigger = true の場合、プレイヤー接近判定が行えません。\n' +
'エディタで Add Component → Physics2D → Collider2D (BoxCollider2D など) を追加し、' +
'IsTrigger を ON にすることを推奨します。');
} else {
// Trigger イベント
this._collider2D.on('onTriggerEnter', this._onTriggerEnter, this);
this._collider2D.on('onTriggerExit', this._onTriggerExit, this);
// 衝突イベント(Trigger でない場合のフォールバック)
this._collider2D.on('onBeginContact', this._onBeginContact, this);
this._collider2D.on('onEndContact', this._onEndContact, this);
}
this._rigidBody2D = this.getComponent(RigidBody2D);
if (!this._rigidBody2D) {
warn('[KeyDoor] このノードに RigidBody2D がアタッチされていません。\n' +
'2D物理挙動やトリガー判定を安定させるため、Add Component → Physics2D → RigidBody2D の追加を推奨します。' +
'(Type: Static に設定するとドアとして扱いやすくなります)');
}
}
private _onTriggerEnter(selfCollider: Collider2D, otherCollider: Collider2D, contact?: IPhysics2DContact | null) {
if (this.debugLog) {
log('[KeyDoor] onTriggerEnter - other:', otherCollider.node.name);
}
this._playerInside = true;
}
private _onTriggerExit(selfCollider: Collider2D, otherCollider: Collider2D) {
if (this.debugLog) {
log('[KeyDoor] onTriggerExit - other:', otherCollider.node.name);
}
this._playerInside = false;
}
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact?: IPhysics2DContact | null) {
if (this.debugLog) {
log('[KeyDoor] onBeginContact - other:', otherCollider.node.name);
}
this._playerInside = true;
}
private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact?: IPhysics2DContact | null) {
if (this.debugLog) {
log('[KeyDoor] onEndContact - other:', otherCollider.node.name);
}
this._playerInside = false;
}
// ===== 入力処理 =====
private _onKeyDown(event: EventKeyboard) {
if (event.keyCode !== this.useKeyCode) {
return;
}
// トリガー内のみ受付する設定の場合
if (this.requireTrigger && !this._playerInside) {
if (this.debugLog) {
log('[KeyDoor] プレイヤーがドアのトリガー内にいないため、入力を無視しました。');
}
return;
}
// すでにアニメーション中なら無視
if (this._isAnimating) {
if (this.debugLog) {
log('[KeyDoor] ドアアニメーション中のため、入力を無視しました。');
}
return;
}
// 閉じる処理が許可されているか
if (this._isOpen) {
if (this.canClose) {
this.closeDoor();
} else {
if (this.debugLog) {
log('[KeyDoor] ドアはすでに開いており、canClose = false のため再度は閉じられません。');
}
}
return;
}
// ドアを開ける処理
if (!this._hasRequiredItems()) {
if (this.debugLog) {
log(`[KeyDoor] 必要なアイテム "${this.requiredItemName}" を ${this.requiredItemCount} 個所持していないため、ドアは開きません。現在: ${this._dumpInventory()}`);
}
return;
}
const consumed = this._consumeRequiredItems();
if (!consumed) {
// 理論上ここには来ないが、防御的にチェック
warn('[KeyDoor] _hasRequiredItems() は true でしたが、_consumeRequiredItems() に失敗しました。');
return;
}
this.openDoor();
}
// ===== ドア開閉アニメーション =====
public openDoor() {
if (this._isOpen) {
return;
}
this._isOpen = true;
if (this.debugLog) {
log('[KeyDoor] ドアを開きます。mode =', DoorMode[this.doorMode]);
}
switch (this.doorMode) {
case DoorMode.NONE:
// 見た目の変化なし
break;
case DoorMode.MOVE:
this._animateMove(true);
break;
case DoorMode.ROTATE:
this._animateRotate(true);
break;
}
}
public closeDoor() {
if (!this._isOpen) {
return;
}
this._isOpen = false;
if (this.debugLog) {
log('[KeyDoor] ドアを閉じます。mode =', DoorMode[this.doorMode]);
}
switch (this.doorMode) {
case DoorMode.NONE:
// 見た目の変化なし
break;
case DoorMode.MOVE:
this._animateMove(false);
break;
case DoorMode.ROTATE:
this._animateRotate(false);
break;
}
}
private _animateMove(open: boolean) {
this._isAnimating = true;
const fromPos = this.node.position.clone();
const target = open
? this._originalPosition.clone().add(this.openMoveOffset)
: this._originalPosition.clone();
tween(this.node)
.to(this.openDuration, { position: target })
.call(() => {
this._isAnimating = false;
if (this.debugLog) {
log('[KeyDoor] MOVE アニメーション完了。open =', open);
}
})
.start();
}
private _animateRotate(open: boolean) {
this._isAnimating = true;
const fromEuler = this.node.eulerAngles.clone();
const targetEuler = open
? this._originalEuler.clone().add(this.openRotateEuler)
: this._originalEuler.clone();
tween(this.node)
.to(this.openDuration, { eulerAngles: targetEuler })
.call(() => {
this._isAnimating = false;
if (this.debugLog) {
log('[KeyDoor] ROTATE アニメーション完了。open =', open);
}
})
.start();
}
}
コードの要点解説
onLoad- ノードの初期位置・回転を保存(開閉時の基準値)。
initialInventoryJsonをパースして内部インベントリを構築。Collider2D/RigidBody2Dの存在をチェックし、あればイベント登録、なければ警告ログ。- キーボード入力(
Input.EventType.KEY_DOWN)を購読。
- インベントリ処理
_inventory: Map<string, number>でアイテム名と所持数を管理。_hasRequiredItems()で必要数を満たしているか判定。_consumeRequiredItems()で必要数だけ減算し、0 以下になったら削除。
- コライダーイベント
onTriggerEnter/onTriggerExitまたはonBeginContact/onEndContactで_playerInsideを制御。requireTrigger = trueの場合、_playerInsideが true のときのみキー入力を受け付ける。
- 入力処理(
_onKeyDown)- 指定キー以外は無視。
requireTrigger&&!_playerInsideのときは無視。- アニメーション中は多重入力を無視。
- ドアが開いている場合:
canClose = trueならcloseDoor()。- そうでなければ何もしない。
- 閉じている場合:
- インベントリに必要アイテムがなければログを出して終了。
- あれば
_consumeRequiredItems()で消費し、openDoor()実行。
- ドア開閉アニメーション
doorMode = NONE: 見た目の変化なしで論理状態のみ変更。doorMode = MOVE:_originalPositionとopenMoveOffsetを使ってtweenで移動。doorMode = ROTATE:_originalEulerとopenRotateEulerを使ってtweenで回転。_isAnimatingフラグで多重アニメーションを防止。
使用手順と動作確認
1. スクリプトファイルの作成
- Editor の Assets パネルで右クリックします。
- Create → TypeScript を選択し、ファイル名を
KeyDoor.tsにします。 - 自動生成されたコードをすべて削除し、前述の
KeyDoorのコードを貼り付けて保存します。
2. テスト用シーンとドアノードの準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ドアとして使うノードを作成します。
- 分かりやすい名前に変更します(例:
Door_Key_1)。
3. 物理コンポーネントの追加(推奨)
プレイヤーとの接触判定に 2D 物理を利用します。
- ドアノードを選択し、Inspector パネルで Add Component ボタンを押します。
- Physics 2D → BoxCollider2D など、任意の
Collider2Dを追加します。 - Collider2D の設定:
- IsTrigger にチェックを入れます(トリガーとして使う)。
- サイズ(Size)を調整して、プレイヤーが接触しやすい範囲に広げます。
- 同じく Add Component → Physics 2D → RigidBody2D を追加します。
- Type を Static に設定します(ドアは動かない物体として扱う)。
※ コライダーやリジッドボディがなくてもスクリプト自体は動作しますが、requireTrigger = true の場合は接触判定ができないため、実用上は追加を推奨します。
4. KeyDoor コンポーネントのアタッチ
- ドアノードを選択した状態で、Inspector の Add Component ボタンを押します。
- Custom Component → KeyDoor を選択してアタッチします。
5. インスペクタでプロパティを設定
ドアノードにアタッチされた KeyDoor コンポーネントの各プロパティを設定します。
基本的な例(鍵1個で開くスライドドア)
- Initial Inventory Json:
{"Key": 1}- このドア自身が「Key を1個だけ持っている」状態から開始します(サンプル用)。
- プレイヤーインベントリがまだない場合のテスト用として便利です。
- Required Item Name:
Key - Required Item Count:
1 - Use Key Code:
KEY_Eなど、任意のキーを選択。 - Door Mode:
MOVE - Open Move Offset:
(0, 200, 0)- 上方向に 200 移動してドアが「上にスライドして開く」ように見せます。
- Open Rotate Euler: デフォルトのまま(
(0, 90, 0))で問題ありません(今回は MOVE モードなので未使用)。 - Open Duration:
0.5(0.3〜0.8 の間で好みに調整)。 - Can Close:
false(一度開いたら閉じないドア)。 - Require Trigger:
true(ドアに接触しているときだけ開く)。 - Debug Log:
true(動作確認時は ON を推奨)。
6. プレイヤー(またはテストオブジェクト)の準備
シンプルな動作確認用に、プレイヤー役のノードを用意します。
- Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、
Playerノードを作成します。 - Add Component → Physics 2D → BoxCollider2D を追加します。
- IsTrigger のチェックは外しておきます(通常の物体として扱う)。
- Add Component → Physics 2D → RigidBody2D を追加します。
- Type を Dynamic に設定します。
- 簡単な移動スクリプト(左右矢印キーで動くなど)を追加してもよいですが、テストだけなら「Scene ビュー上で Player をドラッグして Door に重ねる → Game を再生 → キーを押す」という手順でも確認できます。
7. 実行して動作確認
- Editor 上部の Play ボタン(▶)を押してシーンを再生します。
- Scene ビューまたは Game ビューで、
PlayerノードをDoorノードに近づけ、コライダー同士が重なる位置に移動させます。 - Eキー(Use Key Code で設定したキー)を押します。
- 期待される挙動:
- コンソールに以下のようなログが表示されます(
Debug Log = trueの場合)。[KeyDoor] アイテム "Key" を 1 個消費しました。残り: { }[KeyDoor] ドアを開きます。mode = MOVE[KeyDoor] MOVE アニメーション完了。open = true
- ドアノードが
Open Move Offsetで指定した分だけスムーズに移動し、開いたように見えます。 - 再度 Eキーを押しても、
Can Close = falseのため何も起こりません。
- コンソールに以下のようなログが表示されます(
8. 鍵が足りない場合の確認
鍵が足りない場合の挙動も確認しておきましょう。
- ドアノードの
Initial Inventory Jsonを{"Key": 0}または空文字に変更します。 - 再度シーンを再生し、プレイヤーをドアに近づけて Eキーを押します。
- コンソールに以下のようなログが表示され、ドアは開きません。
[KeyDoor] 必要なアイテム "Key" を 1 個所持していないため、ドアは開きません。現在: { }
9. 回転ドアモードの確認(オプション)
横開きドアのような表現に切り替える場合:
- ドアノードの
Door Modeを ROTATE に変更します。 Open Rotate Eulerを(0, 90, 0)に設定(ヨー軸に90度回転)。Open Move Offsetは未使用なのでそのままで構いません。- 再生して Eキーを押すと、ドアが回転して開くように見えます。
まとめ
この KeyDoor コンポーネントは、
- 内部に簡易インベントリを持つことで、外部の GameManager や Inventory システムに一切依存せず、単体で「鍵付きドア」ギミックを実現します。
- 必要な設定(鍵アイテム名・必要数・入力キー・アニメーション方式・トリガーの有無など)はすべて インスペクタから調整可能 で、シーン内のどのドアにもコピペで再利用できます。
- MOVE / ROTATE / NONE の 3 モードに対応しているため、スライドドア・横開きドア・論理的なゲートなど、さまざまな表現に応用できます。
実運用では、
- 今回の内部インベントリ部分を、将来的に「グローバルなプレイヤーインベントリ」と差し替える。
- ドアが開いたタイミングで SE 再生やパーティクル再生を追加する。
- 特定のタグを持つオブジェクトのみを「プレイヤー」とみなすように拡張する。
といった発展も容易です。
まずはこの汎用 KeyDoor をそのままゲームに組み込んでみて、鍵・扉・ゲートなどのギミックを手早く量産してみてください。




