【Cocos Creator 3.8】PushableBlock の実装:アタッチするだけで「プレイヤーが押した時だけ動く岩」を実現する汎用スクリプト
このガイドでは、CharacterBody2D を使った 2D アクションなどで使える「押せる岩」コンポーネント PushableBlock を実装します。
任意のノードにこのスクリプトをアタッチし、最低限の物理設定を行うだけで、プレイヤーが横から押したときだけ動く岩 を簡単に作れるようになります。
- プレイヤー以外のオブジェクト(敵や弾など)が当たっても動かしたくない
- プレイヤーが上に乗ったり、ぶつかったりしても「押す」方向でのみ動かしたい
- ゲームごとに押せる重さ・摩擦感を Inspector から調整したい
といった場面で、そのノード単体だけで完結する汎用コンポーネントとして利用できます。
コンポーネントの設計方針
要件整理
- CharacterBody2D を持つ 2D オブジェクト にアタッチする。
- プレイヤーが横方向から接触して「押している」間だけ、岩が押された方向に動く。
- プレイヤーの判定は「特定のグループ名」または「特定のタグ名」を Inspector から指定して行う。
- プレイヤーが押していないときは、岩は慣性で少しだけ動いて徐々に止まる(摩擦)ようにできる。
- 外部の GameManager やプレイヤースクリプトに依存せず、このコンポーネントだけで完結させる。
物理挙動は CharacterBody2D の velocity を直接制御する形で実装し、「押されているかどうか」 の検出には、onBeginContact / onEndContact または onCollisionEnter / onCollisionExit を利用します。
Cocos Creator 3.8 では 2D 物理に Box2D と Cannon などのバックエンドがありますが、ここでは標準的な Physics2D(Box2D)を前提に、Collider2D のコンタクトコールバックでプレイヤー接触を検出します。
Inspector で設定可能なプロパティ設計
PushableBlock コンポーネントに用意するプロパティと役割は次の通りです。
- playerGroupName: string
- プレイヤーが属する 2D 物理グループ名(例:
"player")。 - ここで指定されたグループのノードとの接触のみ「押し」とみなします。
- 空文字の場合はグループによるフィルタを行わず、
playerTagだけで判定します。
- プレイヤーが属する 2D 物理グループ名(例:
- playerTag: string
- プレイヤーノードに設定しておく
Node.nameもしくはNode.layerとは別の、単純な名前判定用の文字列。 - ここでは簡易的に「相手ノードの名前がこの文字列を含む場合」をプレイヤーとみなす例を示します(例:
"Player")。 - 空文字の場合はタグによるフィルタを行わず、
playerGroupNameだけで判定します。
- プレイヤーノードに設定しておく
- moveSpeed: number
- 押されたときに岩が動く 最大速度(単位: 単位/秒)。
- 大きいほど軽く押せる岩、小さいほど重い岩になります。
- 例:
1.5 ~ 5.0程度で調整。
- acceleration: number
- 押されたときに速度が増加する 加速度(単位: 単位/秒^2)。
- 大きいほど押し始めからスッと動き、小さいほどじわじわ動き出します。
- friction: number
- プレイヤーが押していないときに速度が減衰する係数。
0 ~ 1の範囲で、1に近いほどよく滑り、0に近いほどすぐ止まります。- 実装では
velocity.x *= frictionのように毎フレーム乗算します。
- minVelocityThreshold: number
- この値よりも速度が小さくなったら
0に丸めて完全に停止させるためのしきい値。 - 小さすぎるといつまでも微小な速度が残るため、
0.01などに設定します。
- この値よりも速度が小さくなったら
- debugLog: boolean
- オンにすると、プレイヤー接触の開始・終了や、必須コンポーネントが見つからない場合などに詳細なログを出力します。
- デバッグ時のみ有効にし、リリース時はオフにする想定です。
必要な標準コンポーネント
このスクリプトが正しく動作するためには、アタッチ先のノードに次のコンポーネントが必要です。
- CharacterBody2D
- 岩の移動を制御するために必須です。
- 見つからない場合、スクリプト内でエラーログを出し、処理を中断します。
- Collider2D(BoxCollider2D など)
- プレイヤーとの接触判定に必須です。
- 見つからない場合もエラーログを出し、処理を中断します。
これらはコード内で getComponent して存在確認を行い、足りない場合はログに明示的なメッセージを出すようにします。
TypeScriptコードの実装
import { _decorator, Component, Node, CharacterBody2D, Collider2D, IPhysics2DContact, Contact2DType, Vec2, Vec3, log, error } from 'cc';
const { ccclass, property } = _decorator;
/**
* PushableBlock
* プレイヤーが横から押したときだけ動く 2D ブロック。
* - CharacterBody2D の velocity.x を制御して横方向の移動のみ行う。
* - プレイヤーとの接触を Collider2D のコールバックで検出する。
*/
@ccclass('PushableBlock')
export class PushableBlock extends Component {
@property({
tooltip: 'プレイヤーが属する 2D 物理グループ名。\n' +
'空文字の場合はグループ名ではフィルタせず、playerTag だけで判定します。\n' +
'例: "player"'
})
public playerGroupName: string = 'player';
@property({
tooltip: 'プレイヤーノード名に含まれる文字列。\n' +
'空文字の場合は名前ではフィルタせず、playerGroupName だけで判定します。\n' +
'例: "Player"'
})
public playerTag: string = 'Player';
@property({
tooltip: '押されたときにブロックが動く最大速度(単位/秒)。\n' +
'大きいほど軽く押せるブロックになります。'
})
public moveSpeed: number = 3.0;
@property({
tooltip: '押されたときに速度が増加する加速度(単位/秒^2)。\n' +
'大きいほど押し始めからスッと動きます。'
})
public acceleration: number = 10.0;
@property({
tooltip: 'プレイヤーが押していないときの減速係数。\n' +
'0 ~ 1 の範囲で、1 に近いほどよく滑り、0 に近いほどすぐ止まります。'
})
public friction: number = 0.85;
@property({
tooltip: 'この値より小さい速度になったとき、完全に停止させるためのしきい値。'
})
public minVelocityThreshold: number = 0.01;
@property({
tooltip: 'デバッグログを出力するかどうか。'
})
public debugLog: boolean = false;
private _body: CharacterBody2D | null = null;
private _collider: Collider2D | null = null;
// 現在プレイヤーに押されているかどうか
private _isPushed: boolean = false;
// プレイヤーが押している方向(-1: 左向きに押されている, 1: 右向きに押されている)
private _pushDirection: number = 0;
onLoad() {
// 必須コンポーネントを取得
this._body = this.getComponent(CharacterBody2D);
if (!this._body) {
error('[PushableBlock] CharacterBody2D が見つかりません。このコンポーネントを使うノードには CharacterBody2D を追加してください。');
}
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
error('[PushableBlock] Collider2D が見つかりません。このコンポーネントを使うノードには BoxCollider2D などの Collider2D を追加してください。');
}
if (this._collider) {
// コンタクトイベントの登録
this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
this._collider.on(Contact2DType.END_CONTACT, this._onEndContact, this);
}
}
start() {
if (this.debugLog) {
log('[PushableBlock] start: initialized on node', this.node.name);
}
}
onDestroy() {
if (this._collider) {
this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
this._collider.off(Contact2DType.END_CONTACT, this._onEndContact, this);
}
}
/**
* プレイヤーとの接触開始時に呼ばれる。
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
const otherNode = otherCollider.node;
if (!this._isPlayer(otherNode)) {
return;
}
// プレイヤーがどちら側から押しているかを判定する
const blockPos = this.node.worldPosition;
const playerPos = otherNode.worldPosition;
// プレイヤーがブロックの左側にいれば、右向きに押されている (= ブロックは正方向に動く)
const dir = playerPos.x < blockPos.x ? 1 : -1;
this._pushDirection = dir;
this._isPushed = true;
if (this.debugLog) {
log(`[PushableBlock] BEGIN_CONTACT with player: ${otherNode.name}, pushDirection=${this._pushDirection}`);
}
}
/**
* プレイヤーとの接触終了時に呼ばれる。
*/
private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
const otherNode = otherCollider.node;
if (!this._isPlayer(otherNode)) {
return;
}
this._isPushed = false;
this._pushDirection = 0;
if (this.debugLog) {
log(`[PushableBlock] END_CONTACT with player: ${otherNode.name}`);
}
}
/**
* 対象ノードがプレイヤーかどうかを判定する。
* - playerGroupName が設定されていればグループ名でチェック
* - playerTag が設定されていれば名前に含まれるかをチェック
* どちらも設定されていれば AND 条件ではなく OR 条件で判定する。
*/
private _isPlayer(node: Node): boolean {
let isByGroup = false;
let isByTag = false;
if (this.playerGroupName.trim().length > 0) {
// PhysicsSystem2D では group は数値ですが、ここでは Node のグループ名を使う簡易実装にします。
// 実際のプロジェクトでは 2D 物理グループのビットマスク設定に合わせて判定してください。
if (node.group === this.playerGroupName) {
isByGroup = true;
}
}
if (this.playerTag.trim().length > 0) {
if (node.name.indexOf(this.playerTag) !== -1) {
isByTag = true;
}
}
// どちらも未設定の場合は常に false
if (this.playerGroupName.trim().length === 0 && this.playerTag.trim().length === 0) {
return false;
}
return isByGroup || isByTag;
}
update(deltaTime: number) {
if (!this._body) {
return;
}
const v = this._body.velocity;
if (this._isPushed && this._pushDirection !== 0) {
// 押されている間は加速度を加えていく
let targetVx = v.x + this.acceleration * this._pushDirection * deltaTime;
// 最大速度を制限
if (targetVx > this.moveSpeed) {
targetVx = this.moveSpeed;
} else if (targetVx < -this.moveSpeed) {
targetVx = -this.moveSpeed;
}
v.x = targetVx;
} else {
// 押されていないときは摩擦で減速
v.x *= this.friction;
// しきい値以下なら完全停止
if (Math.abs(v.x) < this.minVelocityThreshold) {
v.x = 0;
}
}
// 縦方向の速度はそのまま維持(重力などが働いている場合を考慮)
this._body.velocity = v;
}
}
コードのポイント解説
- onLoad
CharacterBody2DとCollider2DをgetComponentで取得し、存在しない場合はerrorログを出します。Collider2Dが存在する場合、Contact2DType.BEGIN_CONTACTとEND_CONTACTのリスナーを登録します。
- _onBeginContact / _onEndContact
- 接触した相手がプレイヤーかどうかを
_isPlayerで判定します。 - プレイヤーとの接触開始時に
_isPushed = trueとし、プレイヤーの位置から押す方向(左 or 右)を決定して_pushDirectionに格納します。 - 接触終了時には
_isPushed = false,_pushDirection = 0に戻します。
- 接触した相手がプレイヤーかどうかを
- _isPlayer
playerGroupNameとplayerTagの両方(どちらか一方でも可)で、相手ノードがプレイヤーかどうかを判定します。- どちらも空文字の場合は常に
falseを返し、誤判定を防ぎます。
- update
- 毎フレーム
CharacterBody2D.velocityを更新します。 - 押されている間:
accelerationに基づいてvelocity.xを増減させ、moveSpeedを超えないようクランプします。 - 押されていない間:
frictionを乗算して減速させ、minVelocityThreshold未満になったら 0 に丸めます。 - 縦方向の速度
velocity.yはそのまま維持することで、落下などの他の物理挙動を邪魔しません。
- 毎フレーム
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create > TypeScript を選択し、ファイル名を
PushableBlock.tsとして作成します。 - 自動生成されたテンプレートコードをすべて削除し、前節の
PushableBlockのコードを丸ごと貼り付けて保存します。
2. 押せる岩用ノードの作成
- Hierarchy パネルで右クリックし、Create > 2D Object > Sprite などを選択して、岩用のノードを作成します。
- 名前の例:
PushableRock - 任意の岩の画像(スプライト)を設定します。
- 名前の例:
- 作成した岩ノードを選択し、Inspector で次のコンポーネントを追加します。
- Add Component > Physics 2D > CharacterBody2D
- Add Component > Physics 2D > BoxCollider2D(または適切な形状の
Collider2D)- 岩の見た目に合わせてサイズを調整します。
- 同じく Inspector で Add Component > Custom > PushableBlock を選択して、このスクリプトをアタッチします。
3. プレイヤーノードの準備
プレイヤー側も 2D 物理で動いている想定です。最小限の設定例を示します。
- Hierarchy で Create > 2D Object > Sprite を選択して、プレイヤーノード(例:
Player)を作成します。 - Inspector で次のコンポーネントを追加します。
- Physics 2D > CharacterBody2D
- Physics 2D > BoxCollider2D(または他の Collider2D)
- プレイヤーノードの Group を
playerに設定します。- Group 設定はメインメニューの Project > Project Settings > Groups から追加できます。
- ここで
"player"というグループを作成し、プレイヤーノードに割り当ててください。
- プレイヤーノードの Name を
Playerにしておくと、playerTag = "Player"で判定しやすくなります。
4. PushableBlock のプロパティ設定
岩ノードを選択し、Inspector の PushableBlock コンポーネントのプロパティを次のように設定します(例)。
- Player Group Name:
player - Player Tag:
Player - Move Speed:
3.0 - Acceleration:
10.0 - Friction:
0.85 - Min Velocity Threshold:
0.01 - Debug Log: 開発中は
ONにしてログを確認、安定したらOFFにする
この設定では、プレイヤーが岩に横から接触している間だけ、徐々に加速しながら最大速度 3.0 まで動き、離れると摩擦で少し滑って止まるような挙動になります。
5. 物理システムの有効化と動作確認
- メインメニューから Project > Project Settings を開きます。
- Physics 2D タブを開き、物理システムが有効になっていることを確認します。
- Enable がチェックされているか。
- 必要に応じて重力などを設定します(例:
Gravity: (0, -320))。
- Scene を保存し、Preview(再生ボタン) を押して実行します。
- プレイヤーを操作して、岩に横からぶつかるように動かします。
- プレイヤーが岩に接触している間、岩がゆっくりと押されて動くこと。
- プレイヤーが離れると、岩が徐々に減速して止まること。
- プレイヤー以外のオブジェクト(敵や弾など)をぶつけても岩が動かないこと。
- Debug Log を ON にしている場合、コンソールに次のようなログが出力されることを確認します。
[PushableBlock] BEGIN_CONTACT with player: Player, pushDirection=1[PushableBlock] END_CONTACT with player: Player
もし岩が全く動かない場合は、次を確認してください。
- 岩ノードに CharacterBody2D と Collider2D が正しく追加されているか。
- プレイヤーノードの Group が
playerになっているか。 - プレイヤーノードの Name に
"Player"が含まれているか。 - Physics 2D が有効になっているか。
まとめ
この PushableBlock コンポーネントは、
- CharacterBody2D + Collider2D を持つ任意のノードにアタッチするだけで「押せる岩」ギミックを実現できる、独立した汎用スクリプトです。
- プレイヤー判定は グループ名とタグ文字列で柔軟に行えるため、プロジェクト構成に合わせて簡単に調整できます。
- 重さ・滑りやすさ・加速感 をすべて Inspector から調整できるため、ゲームごとの手触りに合わせたチューニングが容易です。
応用例としては、
- スイッチの上に乗せると仕掛けが動く「仕掛け用ブロック」
- 落とし穴に落とすことで足場を作る「落とせる岩」
- 複数人協力で押すと動く「協力ギミック」
など、さまざまなパズル・アクション要素に発展させられます。
このコンポーネントは外部のマネージャやシングルトンに依存していないため、別プロジェクトへの持ち込みや、チーム内での共有もしやすい構成になっています。
まずはそのまま使い、慣れてきたら「縦方向にも押せるようにする」「押している間だけ効果音を鳴らす」など、プロジェクトに合わせた拡張に挑戦してみてください。




