【Cocos Creator 3.8】TentacleLimb(多関節触手)の実装:アタッチするだけで「物理でうねる触手」を自動生成する汎用スクリプト
このガイドでは、TentacleLimb というコンポーネントを実装します。
任意のノードにアタッチするだけで、その子として複数のセグメント(節)ノードと 2D 物理ジョイントを自動生成し、物理演算でうねうね動く触手 を表現できる汎用スクリプトです。
プレイヤーの周りで揺れる触手、ボスキャラの腕、草やツタのようなオブジェクトなど、「多関節で柔らかく揺れる」 表現をしたい場面で使えます。
インスペクタから本数・長さ・剛性・揺れの強さなどを調整でき、他のカスタムスクリプトに一切依存せず、このコンポーネント単体で完結 します。
コンポーネントの設計方針
1. 全体の構造
- このコンポーネントをアタッチしたノード を「付け根」(root)とみなし、そこから複数の「セグメント」ノードを自動生成します。
- 各セグメントには
RigidBody2DとBoxCollider2Dを付与し、隣り合うセグメント同士をDistanceJoint2Dで接続して「ひも状の物理オブジェクト」を作ります。 - 付け根ノードには
RigidBody2Dを付け、最初のセグメントと接続します(付け根を静的にするか動的にするかはプロパティで選択)。 - 任意のスプライト画像を 1 枚指定し、それを各セグメントに貼り付けて「触手の節」に見せます。
- 外部スクリプトへの依存は一切なし。必要な設定はすべて
@propertyでインスペクタから指定します。
2. 防御的な実装方針
- 必須の 2D 物理コンポーネント(
RigidBody2D,BoxCollider2D,DistanceJoint2D)は、コード内で自動的に追加します。 - 2D 物理シミュレーションを利用するため、プロジェクトで「2D Physics」を有効にしている前提 ですが、有効でない場合に備え、ログで注意喚起します。
- 必須の
SpriteFrameが指定されていない場合は、セグメントは生成するがスプライトは付けず、警告ログを出します。 - パラメータが不正(例: セグメント数 <= 0, 長さ <= 0)な場合は、生成を行わずエラーログを出します。
3. インスペクタで設定可能なプロパティ
以下のように設計します。
- segmentCount(セグメント数)
- 型:
number - 説明: 触手を構成する節の数。2〜50 程度を想定。
- 例: 10 にすると、付け根の先に 10 個のセグメントが連なります。
- 型:
- segmentLength(1 セグメントの長さ)
- 型:
number - 説明: 各セグメントの縦方向の長さ(ローカル Y 軸正方向に伸びる)。
- 例: 20 にすると、10 セグメントで約 200 ピクセルの触手になります。
- 型:
- segmentThickness(太さ)
- 型:
number - 説明: コライダーと描画スプライトの幅(X 方向)。
- 例: 10 にすると、細めの触手になります。
- 型:
- segmentSprite(セグメント用 SpriteFrame)
- 型:
SpriteFrame - 説明: 各セグメントに貼り付ける画像。未指定の場合はスプライトなしで生成。
- 例: 細長い矩形テクスチャを指定すると自然な見た目になります。
- 型:
- useRootAsStatic(付け根を固定するか)
- 型:
boolean - 説明: true の場合、付け根の
RigidBody2Dを Static にしてワールドに固定します。false の場合は Dynamic にして他の力で動かせます。
- 型:
- segmentMass(質量)
- 型:
number - 説明: 各セグメントの質量。大きいほど重くなり、揺れがゆっくりになります。
- 型:
- segmentLinearDamping(線形減衰)
- 型:
number - 説明: 空気抵抗のような減衰。大きいほどすぐに揺れが収まります。
- 型:
- segmentAngularDamping(角度減衰)
- 型:
number - 説明: 回転に対する減衰。大きいほどクネクネが抑えられます。
- 型:
- jointFrequency(ジョイント剛性)
- 型:
number - 説明:
DistanceJoint2Dの frequencyHz。大きいほどバネが固くなり、ピンと張った触手になります。
- 型:
- jointDampingRatio(ジョイント減衰)
- 型:
number(0〜1 推奨) - 説明: バネの減衰比。0 でバネバネ、1 でほぼ臨界減衰。
- 型:
- autoDrive(自動うねりを有効にするか)
- 型:
boolean - 説明: true の場合、Update 内で各セグメントに周期的な力を加えて自動的にうねらせます。
- 型:
- driveForce(うねりの強さ)
- 型:
number - 説明: 自動うねりで加える力の大きさ。大きいほど激しく揺れます。
- 型:
- driveFrequency(うねりの速度)
- 型:
number - 説明: 自動うねりの時間周波数。大きいほど速くクネクネします。
- 型:
- randomizePhase(各セグメントの位相をずらす)
- 型:
boolean - 説明: true の場合、各セグメントごとにランダムな位相を持たせ、より自然なうねりにします。
- 型:
- clearOnRebuild(再生成時に古いセグメントを削除)
- 型:
boolean - 説明: true の場合、
onLoadなどで再構築する前に、以前に自動生成した子ノードを削除します。
- 型:
TypeScriptコードの実装
以下が、Cocos Creator 3.8.7 用の完全なコンポーネントコードです。
import { _decorator, Component, Node, Sprite, SpriteFrame, RigidBody2D, BoxCollider2D, DistanceJoint2D, PhysicsSystem2D, ERigidBody2DType, Vec2, math } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('TentacleLimb')
export class TentacleLimb extends Component {
@property({
tooltip: '触手を構成するセグメント(節)の数。\n2〜50 程度を推奨します。'
})
public segmentCount: number = 10;
@property({
tooltip: '1 セグメントあたりの長さ(ローカル Y 方向)。\n触手全体の長さは segmentCount × segmentLength になります。'
})
public segmentLength: number = 20;
@property({
tooltip: 'セグメントの太さ(コライダーとスプライトの幅)。'
})
public segmentThickness: number = 10;
@property({
tooltip: '各セグメントに貼り付ける SpriteFrame。\n未指定の場合、スプライトは付与されません。'
})
public segmentSprite: SpriteFrame | null = null;
@property({
tooltip: '付け根の RigidBody2D を Static にしてワールドに固定するかどうか。'
})
public useRootAsStatic: boolean = true;
@property({
tooltip: '各セグメントの質量。\n大きいほど重くなり、揺れがゆっくりになります。'
})
public segmentMass: number = 1.0;
@property({
tooltip: '各セグメントの線形減衰(空気抵抗のような減衰)。\n大きいほどすぐに揺れが収まります。'
})
public segmentLinearDamping: number = 0.5;
@property({
tooltip: '各セグメントの角度減衰。\n大きいほど回転が抑えられます。'
})
public segmentAngularDamping: number = 0.5;
@property({
tooltip: 'DistanceJoint2D のバネ剛性(frequencyHz)。\n大きいほどピンと張った触手になります。'
})
public jointFrequency: number = 3.0;
@property({
tooltip: 'DistanceJoint2D の減衰比(0〜1 推奨)。\n0 でバネバネ、1 でほぼ臨界減衰。'
})
public jointDampingRatio: number = 0.7;
@property({
tooltip: 'Update 内で自動的に力を加えて、触手をうねらせるかどうか。'
})
public autoDrive: boolean = true;
@property({
tooltip: '自動うねりで各セグメントに加える力の大きさ。'
})
public driveForce: number = 5.0;
@property({
tooltip: '自動うねりの速度(時間周波数)。\n大きいほど速くクネクネします。'
})
public driveFrequency: number = 1.5;
@property({
tooltip: 'true の場合、各セグメントごとにランダムな位相を与えて自然なうねりにします。'
})
public randomizePhase: boolean = true;
@property({
tooltip: 'true の場合、再生成前に自動生成したセグメントノードを削除します。'
})
public clearOnRebuild: boolean = true;
// 内部管理用
private _segments: Node[] = [];
private _segmentPhases: number[] = [];
private _built: boolean = false;
onLoad() {
// 2D 物理が有効かどうかをチェック
if (!PhysicsSystem2D.instance.enable) {
console.warn('[TentacleLimb] 2D Physics が無効です。Project Settings の Physics で 2D Physics を有効にしてください。');
}
// パラメータチェック
if (this.segmentCount <= 0 || this.segmentLength <= 0 || this.segmentThickness <= 0) {
console.error('[TentacleLimb] segmentCount, segmentLength, segmentThickness は 0 より大きい値を指定してください。');
return;
}
// 再生成時のクリア
if (this.clearOnRebuild) {
this._clearGeneratedSegments();
}
// 付け根の RigidBody2D を準備
this._ensureRootRigidBody();
// 触手の構築
this._buildTentacle();
this._built = true;
}
start() {
// start では特に何もしませんが、
// 他コンポーネントとの兼ね合いで onLoad 後に何かしたい場合はここに追記します。
}
update(deltaTime: number) {
if (!this._built || !this.autoDrive) {
return;
}
// 各セグメントに周期的な力を加える
const t = performance.now() / 1000; // 秒単位の時間
const twoPi = Math.PI * 2;
for (let i = 0; i < this._segments.length; i++) {
const seg = this._segments[i];
const rb = seg.getComponent(RigidBody2D);
if (!rb) {
continue;
}
const phase = this.randomizePhase ? this._segmentPhases[i] : 0;
const wave = Math.sin(twoPi * this.driveFrequency * t + phase);
// ローカル X 方向に力を与えて、左右に揺らす
const forceMagnitude = this.driveForce * wave;
const force = new Vec2(forceMagnitude, 0);
// セグメントの中心に力を加える
rb.applyForceToCenter(force, true);
}
}
/**
* 付け根ノードに RigidBody2D を追加・設定する
*/
private _ensureRootRigidBody() {
let rootBody = this.node.getComponent(RigidBody2D);
if (!rootBody) {
rootBody = this.node.addComponent(RigidBody2D);
console.log('[TentacleLimb] 付け根ノードに RigidBody2D を自動追加しました。');
}
// 付け根のタイプ設定
rootBody.type = this.useRootAsStatic ? ERigidBody2DType.Static : ERigidBody2DType.Dynamic;
// Dynamic の場合、ある程度の減衰を持たせてもよい
if (!this.useRootAsStatic) {
rootBody.linearDamping = this.segmentLinearDamping;
rootBody.angularDamping = this.segmentAngularDamping;
}
}
/**
* 触手セグメントを構築する
*/
private _buildTentacle() {
this._segments = [];
this._segmentPhases = [];
let previousBody: RigidBody2D | null = this.node.getComponent(RigidBody2D);
if (!previousBody) {
console.error('[TentacleLimb] 付け根ノードに RigidBody2D が見つかりません。');
return;
}
for (let i = 0; i < this.segmentCount; i++) {
const segNode = new Node(`TentacleSegment_${i}`);
segNode.setParent(this.node);
// ローカル位置:付け根から下方向(-Y)に伸ばす
const y = -(i + 1) * this.segmentLength;
segNode.setPosition(0, y, 0);
// スプライトの設定(任意)
if (this.segmentSprite) {
const sprite = segNode.addComponent(Sprite);
sprite.spriteFrame = this.segmentSprite;
// セグメントの長さと太さに合わせてサイズを調整
segNode.setScale(this.segmentThickness / 100, this.segmentLength / 100, 1);
} else {
console.warn('[TentacleLimb] segmentSprite が指定されていないため、セグメントにはスプライトが付きません。');
}
// RigidBody2D の設定
const body = segNode.addComponent(RigidBody2D);
body.type = ERigidBody2DType.Dynamic;
body.gravityScale = 1.0;
body.mass = this.segmentMass;
body.linearDamping = this.segmentLinearDamping;
body.angularDamping = this.segmentAngularDamping;
// BoxCollider2D の設定
const collider = segNode.addComponent(BoxCollider2D);
collider.size.set(this.segmentThickness, this.segmentLength);
collider.apply();
// DistanceJoint2D で前のセグメント(または付け根)と接続
const joint = segNode.addComponent(DistanceJoint2D);
joint.connectedBody = previousBody;
joint.autoCalcDistance = true;
joint.frequency = this.jointFrequency;
joint.dampingRatio = this.jointDampingRatio;
// アンカー位置の調整(セグメントの上端と下端を結ぶイメージ)
// 自ノードのローカルアンカーを上端に、接続先のローカルアンカーを下端に設定
joint.anchor.set(0, this.segmentLength * 0.5);
joint.connectedAnchor.set(0, -this.segmentLength * 0.5);
joint.apply();
this._segments.push(segNode);
// 位相をランダムに設定
const phase = math.randomRange(0, Math.PI * 2);
this._segmentPhases.push(phase);
previousBody = body;
}
}
/**
* 自動生成したセグメントノードを削除する
*/
private _clearGeneratedSegments() {
// 以前に生成した TentacleSegment_* ノードを削除
const children = this.node.children.slice();
for (const child of children) {
if (child.name.startsWith('TentacleSegment_')) {
child.destroy();
}
}
this._segments = [];
this._segmentPhases = [];
this._built = false;
}
}
コードの主要なポイント解説
- onLoad()
- 2D 物理が有効かどうかをチェックし、無効なら警告ログを出します。
- セグメント数・長さ・太さが 0 以下の場合はエラーを出し、処理を中断します。
clearOnRebuildが true の場合、以前に自動生成したセグメントノードを削除します。- 付け根ノードに
RigidBody2Dがなければ追加し、useRootAsStaticに応じて Static / Dynamic を設定します。 _buildTentacle()でセグメントとジョイントを生成します。
- update(deltaTime)
autoDriveが true のときだけ動作します。performance.now()を使って時間tを取得し、sin(2π f t + phase)で周期的な値を作成。- 各セグメントの
RigidBody2Dに対して、ローカル X 方向の力をapplyForceToCenterで加え、左右に揺れるようにします。 randomizePhaseが true の場合、各セグメントに異なる位相を持たせて自然なうねりを実現します。
- _ensureRootRigidBody()
- 付け根ノードに
RigidBody2Dがなければ自動追加し、ログで通知します。 useRootAsStaticに応じてERigidBody2DType.StaticかDynamicを設定します。
- 付け根ノードに
- _buildTentacle()
segmentCountの数だけ子ノードTentacleSegment_iを生成し、付け根から下方向に並べます。- 各ノードに
Sprite(任意)、RigidBody2D、BoxCollider2Dを追加します。 DistanceJoint2Dを追加し、直前のセグメント(または付け根)と接続します。autoCalcDistanceを true にして距離を自動計算し、frequency / dampingRatio をプロパティから設定します。- アンカーを調整して、セグメントの上下端を結ぶようにしています。
- _clearGeneratedSegments()
- 子ノードのうち、名前が
TentacleSegment_で始まるものを破棄します。 - 内部配列もクリアし、
_builtフラグを false に戻します。
- 子ノードのうち、名前が
使用手順と動作確認
Cocos Creator 3.8.7 のエディタ上で、このコンポーネントを実際に使う手順を説明します。
1. TypeScript ファイルの作成
- エディタの Assets パネルで任意のフォルダを選択します(例:
assets/scripts)。 - 右クリック → Create → TypeScript を選択します。
- ファイル名を
TentacleLimb.tsにします。 - 自動生成されたスクリプトを開き、上記の
TentacleLimbクラスのコード全文を貼り付けて保存します。
2. テスト用シーンとノードの準備
- 任意のシーンを開きます(例:
Main.scene)。 - Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノードを作成します。
- 作成したノードの名前を
TentacleRootなどに変更します。 - 重要: このノードは 2D 物理空間上に配置されるため、Canvas の子 にするか、2D カメラで見える位置に配置してください。
3. TentacleLimb コンポーネントをアタッチ
- Hierarchy で
TentacleRootノードを選択します。 - Inspector パネルで Add Component → Custom → TentacleLimb を選択してアタッチします。
4. SpriteFrame(テクスチャ)の用意
- 触手の節に使いたい細長い画像(PNG など)を Assets にインポートします。
- 推奨: 縦長の矩形(例: 幅 20px, 高さ 100px 程度)。
- インポートした画像を選択し、Inspector で SpriteFrame が自動生成されていることを確認します。
5. TentacleLimb のプロパティ設定
TentacleRoot ノードを選択し、Inspector の TentacleLimb コンポーネントの各プロパティを設定します。
- segmentCount: 10
- segmentLength: 25
- segmentThickness: 12
- segmentSprite: 先ほど用意した SpriteFrame をドラッグ&ドロップ
- useRootAsStatic: チェック ON(触手の付け根を固定)
- segmentMass: 0.8
- segmentLinearDamping: 0.4
- segmentAngularDamping: 0.4
- jointFrequency: 4.0(少し張り気味)
- jointDampingRatio: 0.6
- autoDrive: チェック ON
- driveForce: 8.0
- driveFrequency: 1.2
- randomizePhase: チェック ON
- clearOnRebuild: チェック ON(デフォルトのままで OK)
6. 2D Physics の有効化を確認
- メニューから Project → Project Settings を開きます。
- 左側のリストから Physics → 2D を選択します。
- Enable 2D Physics が ON になっていることを確認します。
- 必要に応じて重力(例:
(0, -320))などを調整します。
7. シーンの再生と動作確認
- エディタ上部の Play ボタンを押してゲームを再生します。
- シーン内で
TentacleRootの子として、TentacleSegment_0〜TentacleSegment_9が生成されていることを確認します。 - 物理シミュレーションにより、触手が重力で垂れ下がり、自動的に左右にうねる 様子が見えるはずです。
- 揺れが弱い/強すぎる場合は、driveForce と driveFrequency、jointFrequency / jointDampingRatio を調整して好みの挙動に近づけてください。
8. 応用的な使い方の例
- 付け根を動かす
useRootAsStaticのチェックを外して Dynamic にし、別のオブジェクトからTentacleRootを押したり引いたりすると、触手全体が引きずられるように動きます。
- 複数の触手を配置する
TentacleRootノードを複製し、位置や回転を変えて配置すれば、簡単に複数の触手を生やしたオブジェクトを作れます。
- Segment Sprite を変えて別バリエーション
- 別の色や形のテクスチャを割り当てるだけで、ツタ・鎖・ロープ・尻尾など様々な見た目を再利用できます。
まとめ
この TentacleLimb コンポーネントは、任意のノードにアタッチするだけで、
- 指定した本数・長さ・太さのセグメントを自動生成
- 2D 物理(RigidBody2D + DistanceJoint2D)による柔らかい挙動
- SpriteFrame を 1 枚指定するだけで見た目も自動構築
- 自動うねり(力を加えるだけの単純な駆動)
を実現する、完全に独立した汎用コンポーネント です。
外部の GameManager やシングルトンに一切依存せず、パラメータはすべてインスペクタで完結 しているため、
- 別プロジェクトへのコピペ
- チームメンバーへの共有
- Prefab 化して量産
が非常に簡単です。
さらに、今回の設計パターン(「アタッチされたノードを付け根に、子ノード+物理ジョイントを自動生成する」)は、
触手に限らず、鎖・ロープ・橋・ぶら下がりオブジェクト などにも応用できます。
本記事のコードをベースに、セグメントごとに異なる SpriteFrame を割り当てて先端を太く/細くしたり、
先端に攻撃判定やエフェクトを付けるなど、自由に拡張してみてください。




