6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vket Cloud】HeliScriptの新機能で子オブジェクトを相対座標で制御する

6
Last updated at Posted at 2025-12-09

この記事はVR法人HIKKYのアドベントカレンダー10日目の記事です。

はじめに

こんにちは。HIKKYのしらピーです。
Vket Cloudを使ったコンテンツ制作やコンテンツの監修の業務を行っております。

弊社開発のWebブラウザ上で動作するVRエンジン「Vket Cloud」は2025年11月、バージョン16にアップデートし、ワールドの機能制作に便利な新しい開発機能が増えました。

本記事ではバージョン16にてHeliScript 1 にひっそりと追加された、「ローカルトランスフォームメソッド」について紹介します。

用語解説
本記事内では「ノード」という単語がございます。
こちらはVket Cloudの概念でアイテムの子オブジェクトひとつひとつを表します。
image.png
「アイテム」とは、Vket Cloudにおける物体単位の概念で、VKC Item Field UnityコンポーネントやVKC Item Object Unityコンポーネントを付与したUnityオブジェクトにより生成されます。

アイテムとノードの関係性は上記の画像の通りです。

ローカルトランスフォームメソッドとは?

正式名称ではありませんが、SetNodeLocalPos()など、Itemクラスに新しく増えた、ノードがローカルで保持しているPosition、Rotation、ScaleのSetterとGetterのメソッドのことです。
使いこなすことで、アイテムの子オブジェクトが相対パラメータで自由に操作できるようになります。
今までは各ノードを個別アイテムにしたうえで絶対座標指定で動かすか、Animationを作成しないといけなかったため、従来より直感的に実装することができるようになりました。

Vket Cloud SDK公式マニュアルでは、Itemクラスのページに掲載されています。

ローカルトランスフォームメソッド一覧

SetNodeLocalPos(string, Vector3)
指定した名称のノードの座標をローカル座標系で移動させます。

SetNodeLocalRotate(string, Vector3)
指定した名称のノードをローカル座標系で回転させます。

SetNodeLocalScale(string, Vector3)
指定した名称のノードの大きさをローカル座標系で変更させます。

GetNodeLocalPos(string)
指定した名称のノードのローカル座標系座標をVector3クラスで取得します。

GetNodeLocalRotate(string)
指定した名称のノードのローカル座標系回転をQuaternionクラスで取得します。

GetNodeLocalScale(string)
指定した名称のノードのローカル座標系での大きさをVector3クラスで取得します。

注意:VKC Item Fieldで実装したノードは動かせない
VKC Item Field Unityコンポーネントを持ったオブジェクトの子オブジェクトはノードとしてワールド上に現れますが、この方法で実装したノードは動かすことができません。

したがって、Set○○系メソッドを使っても効果がありません。
※Getは可能です。

オブジェクトを動かしたい場合は、フィールドのエクスポートで.heoファイルに書き出したうえで、VKC Item Object Unityコンポーネントを使って実装する必要があります。

公式マニュアル - .heoファイルを出力する
公式マニュアル - VKC Item Object

サンプル①:動くロボット

SetNodeLocalPos()SetNodeLocalRotate()を使って動くロボットのサンプルを作りました。
※画像をクリックするとワールドが立ち上がります。

image.png

  • 常に動くロボットがいます。
  • 球体をクリックすると、手足と頭が分離します。
  • もう一度球体をクリックすると、手足と頭がくっつきます。

ロボットは以下のようなノード階層構造を持つアイテムです。
image.png

スクリプト
// ロボットの動きを管理するスクリプト
component LocalTransform
{
    Item _self; // ロボット自身のアイテム情報
    // 各パーツの座標/回転情報
    Vector3 _itemPos, _bodyPos, _shoulderRotateL, _shoulderRotateR, _elbowRotateL, _elbowRotateR, _legRotateL, _legRotateR, _kneeRotateL, _kneeRotateR, _headRotate;
    float _timer; // 経過時間

    public LocalTransform()
    {
        // アイテム情報を登録
        _self = hsItemGetSelf();
        // 初期情報を登録
        GetInit();
    }

    public void Update()
    {
        // 1フレームの長さが0.1秒を超える場合、何もしない(放置対策)
        if(hsSystemGetDeltaTime() > 0.1){return;}

        // 時間をカウントし、2秒経つと0に戻す(2秒周期)
        _timer += hsSystemGetDeltaTime();
        if(_timer >= 2){
            _timer = 0;
        }

        // 現在時間に応じた動きに変更する
        MoveTransform(_timer);
        SetTransform();
    }

    // 初期座標/回転をGetterローカルトランスフォームメソッドで取得
    void GetInit(){
        // 全体の座標取得はItem.GetPos()
        _itemPos = _self.GetPos();
        // ノードの座標取得はItem.GetNodeLocalPos()
        _bodyPos = _self.GetNodeLocalPos("body");
        // ノードの回転取得はQuaternionで取得されるので、Vector3に変換する
        _shoulderRotateL = _self.GetNodeLocalRotate("shoulder_l").GetEulerVector3();
        _shoulderRotateR = _self.GetNodeLocalRotate("shoulder_r").GetEulerVector3();
        _legRotateL = _self.GetNodeLocalRotate("upperleg_l").GetEulerVector3();
        _legRotateR = _self.GetNodeLocalRotate("upperleg_r").GetEulerVector3();
        _elbowRotateL = _self.GetNodeLocalRotate("elbow_l").GetEulerVector3();
        _elbowRotateR = _self.GetNodeLocalRotate("elbow_r").GetEulerVector3();
        _kneeRotateL = _self.GetNodeLocalRotate("knee_l").GetEulerVector3();
        _kneeRotateR = _self.GetNodeLocalRotate("knee_r").GetEulerVector3();
        _headRotate = _self.GetNodeLocalRotate("head").GetEulerVector3();
    }

    // 時間に応じた変数更新
    void MoveTransform(float timer){
        // 0 ~ 0.5秒 右に移動、顔は左を向く、腕を真横に上げる
        if(timer<0.5){
            _itemPos.x = timer * 2;
            _shoulderRotateL.z = -90 + timer * 180;
            _shoulderRotateR.z = 90 - timer * 180;
            _headRotate.y = timer * 60;
        }
        // 0.5 ~ 1秒 左に移動、顔は正面に戻す、肘を曲げ力こぶを作る、体を少し下におろす
        else if(timer<1){
            _itemPos.x = 1 - ((timer-0.5) * 2);
            _bodyPos.y = -((timer-0.5) * 0.6);
            _elbowRotateL.z = -90 + timer * 180;
            _elbowRotateR.z = 90 - timer * 180;
            _legRotateL.z = (timer - 0.5) * 90;
            _legRotateR.z = (timer - 0.5) * -90;
            _kneeRotateL.z = (timer - 0.5) * -90;
            _kneeRotateR.z = (timer - 0.5) * 90;
            _headRotate.y = (1 - timer) * 60;   
        }
        // 1 ~ 1.5秒 左に移動、顔は右を向く、肘を伸ばし力こぶを解除、体の高さを元に戻す
        else if(timer < 1.5){
            _itemPos.x = 1 - ((timer-0.5) * 2);
            _bodyPos.y = -0.3 + ((timer-1) * 0.6);
            _elbowRotateL.z = 270 - timer * 180;
            _elbowRotateR.z = -270 + timer * 180;
            _legRotateL.z = (1.5 - timer) * 90;
            _legRotateR.z = (1.5 - timer) * -90;
            _kneeRotateL.z = (1.5 - timer) * -90;
            _kneeRotateR.z = (1.5 - timer) * 90;
            _headRotate.y = (1 - timer) * 60;
        }
        // 1.5 ~ 2秒 右に移動、顔は正面に戻す、腕を下におろす
        else{
            _itemPos.x = -1 + ((timer-1.5) * 2);
            _shoulderRotateL.z = 270 - timer * 180;
            _shoulderRotateR.z = -270 + timer * 180;
            _headRotate.y = -30 + (timer - 1.5) * 60;
        }
    }

    // 変数更新を適用する
    void SetTransform(){
        // 全体の移動はItem.SetPos()
        _self.SetPos(_itemPos);
        // ノードの移動はItem.SetNodeLocalPos()
        _self.SetNodeLocalPos("body", _bodyPos);
        // ノードの回転はVector3で指定する必要があるため、
        // QuaternionをVector3に変換したものを使用する
        // hsMathDegToRad()は角度をラジアンに変換できる
        _self.SetNodeLocalRotate("shoulder_l",makeQuaternionZRotation(hsMathDegToRad(_shoulderRotateL.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("shoulder_r",makeQuaternionZRotation(hsMathDegToRad(_shoulderRotateR.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("upperleg_l",makeQuaternionZRotation(hsMathDegToRad(_legRotateL.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("upperleg_r",makeQuaternionZRotation(hsMathDegToRad(_legRotateR.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("elbow_l",makeQuaternionZRotation(hsMathDegToRad(_elbowRotateL.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("elbow_r",makeQuaternionZRotation(hsMathDegToRad(_elbowRotateR.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("knee_l",makeQuaternionZRotation(hsMathDegToRad(_kneeRotateL.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("knee_r",makeQuaternionZRotation(hsMathDegToRad(_kneeRotateR.z)).GetEulerVector3());
        _self.SetNodeLocalRotate("head",makeQuaternionYRotation(hsMathDegToRad(_headRotate.y)).GetEulerVector3());
    }

    // 分離関数
    void Separate(string flag){
        if(flag == "true"){
            // 各パーツをSetNodeLocalPosで移動
            _self.SetNodeLocalPos("shoulder_l", makeVector3(1.5,0.9,0));
            _self.SetNodeLocalPos("shoulder_r", makeVector3(-1.5,0.9,0));
            _self.SetNodeLocalPos("upperleg_l", makeVector3(1.5,-1,0));
            _self.SetNodeLocalPos("upperleg_r", makeVector3(-1.5,-1,0));
            _self.SetNodeLocalPos("head", makeVector3(0,2,0));
        }else{
            // 各パーツをSetNodeLocalPosで元の位置に戻す
            _self.SetNodeLocalPos("shoulder_l", makeVector3(0.5,0.9,0));
            _self.SetNodeLocalPos("shoulder_r", makeVector3(-0.5,0.9,0));
            _self.SetNodeLocalPos("upperleg_l", makeVector3(0.4,-1,0));
            _self.SetNodeLocalPos("upperleg_r", makeVector3(-0.4,-1,0));
            _self.SetNodeLocalPos("head", makeVector3(0,1.25,0));
        }
    }
}

ポイント

  • ローカルトランスフォームメソッドを使うことで動かしながら好きなタイミングで分離・結合を発生させることが可能になります。
  • 応用することでブルドーザーみたいな動きを作ることもできます。

サンプル②:サッカー

※画像をクリックするとワールドが立ち上がります。

image.png

ローカルトランスフォームメソッド一覧にて、「VKC Item Fieldで実装したノードは動かせない」と説明しましたが、物理演算機能を適用したオブジェクトは動かすことができるほか、Getterメソッドでは物理演算によるTransformの変更が適用された値を取得することができます。ただし、Setterメソッドでは相変わらず移動させることはできません。
また、物理演算機能を適用したオブジェクト限定で移動させる関数も存在するため、そちらを利用することで、座標移動を行うことが可能です。

これを使って、簡易的なサッカーゲームを作ってみたものがこのサンプルです。
複数人が同時に空間に入室することはできますが、ボールの位置や得点等の同期は行っていないため、人によってボールの位置が異なることがあります。

物理演算を有効にするための物理エンジンの設定方法については、以下の公式マニュアルのページをご確認ください。

スクリプト
// サッカーゲームスクリプト
component Soccer
{
    list<int> _score; // 得点
    Item _self, _text; // フィールドと得点表記
    bool _isGoaled; // ゴールしたかどうかのフラグ
    float _unflagtimer; // フラグ解除までの時間測定タイマー

    Vector3 _ballPos; // ボールの位置

    public Soccer()
    {
        // 2人分の得点を格納するためのint型リスト作成
        _score = new list<int>(2);

        // フィールドと得点表記のアイテム情報格納
        _self = hsItemGetSelf();
        _text = hsItemGet("Score");
    }

    public void Update()
    {
        // ボールの位置をGetNodeLocalPos()で取得
        _ballPos = _self.GetNodeLocalPos("Ball");

        // ボールがゴールエリアに入ったら、ゴール処理実行
        if(_ballPos.z < -9.4 && _ballPos.x < 3 && _ballPos.x > -3 && _ballPos.y < 3){
            Goal(1);
        }
        if(_ballPos.z > 9.4 && _ballPos.x < 3 && _ballPos.x > -3 && _ballPos.y < 3){
            Goal(2);
        }

        // ゴール処理後0.5秒はゴール処理が発生しないようにする(多重得点防止のため)
        if(_isGoaled){
            _unflagtimer += hsSystemGetDeltaTime();
            if(_unflagtimer > 0.5){
                _isGoaled = false;
                _unflagtimer = 0;
            }
        }
    }

    // ゴール処理
    void Goal(int side){
        if(_isGoaled){return;}
        _isGoaled = true;

        // ゴールが入った側に得点追加(100点でリセット)
        if(side == 1){
            _score[0]++;
            if(_score[0] >= 100){
                _score[0] -= 100;
            }
        }else{
            _score[1]++;
            if(_score[1] >= 100){
                _score[1] -= 100;
            }
        }

        // 得点表記更新
        _text.WriteTextPlane("%02d-%02d" %_score[0] % _score[1]);

        // ボールにかかっている力を消去し、初期位置へ移動
        _self.ClearPhysicsWorldForce("Ball");
        _self.SetPhysicsWorldPos("Ball",makeVector3(0, 5, 0));
    }
}

ポイント
物理演算が有効になったノードに対しては、以下のメソッドが有効です。

  • Item.ClearPhysicsWorldForce(string): 指定したノードに作用している力を消去する
  • Item.SetPhysicsWorldPos(string, Vector3): 第1引数で指定したノードを第2引数で指定した座標に移動させる

その他の物理演算メソッドについては、公式マニュアル - Itemクラスをご確認ください。
※こちらは相対座標制御ではなく、絶対座標制御のメソッドです。

まとめ

ローカルトランスフォームメソッドを使うことでノードの相対Transformを取得、設定することができます。
従来の実装では複雑になっていた、親子構造を持つオブジェクトの移動がシンプルになりました。

ただし、VKC Item Fieldで実装されたアイテムのノードはローカルトランスフォームメソッドのSetterメソッドでは操作することはできません。
物理演算を組み合わせることで、VKC Item Field内のノードを動かすことができるようになり、ローカルトランスフォームメソッドのGetterメソッドと組み合わせることも可能です。

おわりに

Vket Cloudは公式Discordサーバーがあります。

「こういったワールドを作りたいけどどうしたらいいか分からない」、「制作中にこんなエラーが出た!解決方法を教えてほしい」といった、ワールド制作の不明点はこちらのサーバーの質問・要望・不具合報告-forumに投稿していただけると、スタッフが解決方法や実装方法について回答します。お気軽にどうぞ!

  1. HeliScript(ヘリスクリプト) : Vket Cloudで使用可能なC#ベースの独自言語。高度なギミックを作るのに用いられる。入門記事

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?