【VCI入門】スライドを変更するレーザポインタを作る
この記事はVCI Advent Calendar 2021 15日目です。
はじめに
メタバースが話題になっている現在ですが、VirtualCastにはVCIという仕様でアイテムをルーム/スタジオ(所謂ワールド)を超えて持ち運ぶことが出来ます。相互運用のための仕組みですね。この記事はVCIを作る以下のチュートリアルの続きです。前回から大分間が開いてしまいましたが。。。
特に第一回はUnityのダウンロードからザ・シードオンラインへのアプロード、GitHubでコード管理を書いてるので 「Unity触ったことが無い!」 という方も取っつき易い記事になってるのではないかと思います。私がそうだったので私の苦労話が詰まってるはず。第三回の今回はスライドを切り替えるレーザポインタを作成しアイテム間の連携に関して書いていきたいと思います。
今回使ったコードはこちらとなります。
初心者による初心者のためのVCI入門というスタンスの記事なので、誤ってる点があればご指摘頂けると幸いです。
レーザポインタの仕様を考える
前回作ったホワイトボードVCIは画面をタッチすることで画像を切り替える事が出来ました。ただしスライドをめくるためには直接触る必要がありました。これだとちょっと使いづらいですよね? なので、今回は公式のホワイトボードと同じくレーザポインタで切り替えれるようにしていました。
完成品はこんな感じです。
仕様としては以下を考えます。
- レーザっぽいものが伸びる
- レーザポインタ側でUseをする事でスライドが切り替わる
- レーザがスライドに当たる位置で進むだけでは無く、ページを戻すこともできる
- 一つのレーザポインタで複数のスライドに対応できる
割と普通の要件ですね。ただ、地味に面倒なのが実は最後の項目です。これに関しては後ほど書きます。
レーザポインタ用のシーンの作成、オブジェクトの配置
まずは「New Scean」で以下のようにLazerPointer Sceanを作成します。VCIとして独立しているので別のProjectを新規に作成しても良いのですが、関連性が強いアイテムなので私は同じプロジェクト内に別のSceanとして作成しました。
空のオブジェクトを作成し、名前を変更。そしてVCIオブジェクトをコンポーネントとして紐づけます。
続いてレーザポインタのStickとLayzerをを作成します。StickはキューブでVCIサブアイテムを紐づけColliderはBoxColliderを使用します。重要なのはRigidbodyのMassの値を10000など十分に大きい値にすることです。後ほど書きますが、これをしないとレーザを出したときにVCIが揺れまくります。LazerもキューブでColliderはBoxColliderです。Rigidbody/Massは逆に1e-07
と極小にします。
最後にStickの下にDummyLazerを作ります。これはレーザとスティックの動きを同期させるためのハックに必要になるので詳細は後ほど説明します。
レーザを点灯させるギミックを作る
概要
つづいて、レーザを点灯させるギミックを作ります。レーザーが点灯するというギミックを「どのようにVCIで表現するか?」ですが、今回は 「スティックにレーザという別のサブアイテムがくっついて動いてる」、「レーザは表示/非表示が出来る」 というアイデアが基本になっています。これはLuaスクリプトで制御します。以下のようなコードです。
それでは順を追ってみていきましょう。
レーザにスティックの動きを連動させる
VCIで複数オブジェクトを同期させるにはいくつかの方法があります。具体的には以下の3つです。
- Unity上でオブジェクトを親子にする
- Jointでつなげる
- スクリプトで同期させる
それぞれメリット/デメリットがあり詳しくは下記の記事が非常に分かりやすいので要参照。今回は 「どのオブジェクトがuseされたか見分ける必要がある」 という点でスクリプトで同期させる方法を取っています。
スクリプトは以下となります。連動させる振舞いをStickのサブアイテムであるDummyLazerにさせて、そことLazerを同期させてるわけですね。これにより親子関係にあるサブアイテムのように動きを同期させつつ、独立したサブアイテムとしてLayzerを扱う事が出来ます。
function updateAll()
Lazer.obj.SetPosition(DummyLazer.GetPosition())
Lazer.obj.SetRotation(DummyLazer.GetRotation())
Lazer.obj.SetLocalScale(DummyLazer.GetLocalScale())
end
また単にVCIを連動させるだけだと、オブジェクトがめっちゃ暴れました。なので以下に記載があるようにRigidbody/Massの値を極端に重いのと軽いのにする事で、安定化させています。ただ、もしかしたら最終的にFixedJoinからスクリプトによる同期に変えたので、今の設定だともういらない対応かもしれないです。
レーザの点灯(出し入れ)
次はレーザの点灯です。見た目だけでは無く当たり判定も無くしてしまわないといけないですよね。こちらはシンプルに以下のようなコードでレーザの長さ、つまりZ軸を変更しています。これによって伸縮みを実現しています。本当はレーザが別なVCI等に当たったら、そこまでの長さになるようにしたかったのですが、相手の座標を取る方法が上手くいかずとりあえずそれっぽい長さにする仕様にしました。レーザが物体にあたると貫通しますw
function TurnOnLazer()
local scale = Vector3.__new(0.005, 0.005, Lazer.length)
local pos = Vector3.__new(0, 0, Lazer.lightSource)
DummyLazer.SetLocalPosition(pos)
DummyLazer.SetLocalScale(scale)
end
function TurnOffLazer()
local scale = Vector3.__new(0.005, 0.005, 0)
local pos = Vector3.__new(Stick.getLocalPosition().x, Stick.getLocalPosition().y, 0)
DummyLazer.SetLocalPosition(pos)
DummyLazer.SetLocalScale(scale)
end
function onGrab(use)
TurnOnLazer()
end
function onUngrab(use)
TurnOffLazer()
end
スライドを切り替える
最後に本命のスライド切り替えです。VCIではVCI間の情報交換にメッセージを使う事が出来ます。
onUseイベント
の発生時にメッセージをsendFromLazerPointer
という名前で送信します。これでレーザポインタ側の動きをスライド側で取得するわけですね。合わせてスライドVCI側にイベントを受信するvci.message.On("sendFromLazerPointer"...
を追加します。これによってメッセージsendFromLazerPointer
に反応してスライド側でアクションを起こすことが出来ます。
レイザーポインタVCI
function onUse()
vci.message.Emit("sendFromLazerPointer", {event = BoardStatus, paringId = ParingId})
end
スライドVCI
function OnLazerPointerMessage(sender, name, message)
print(string.format("sender ParingId:%s, own ParingId:%s, event:%s", message.paringId, ParingId, message.event))
if (message.paringId == ParingId) then
NextSlide(message.event)
end
end
vci.message.On("sendFromLazerPointer", OnLazerPointerMessage)
続いてスライドの右側を指してるのか左側を指してるのかでスライドを進めるか戻すかを決めれるようにレーザポインタVCIのonTriggerEnter
を変更します。onTriggerEnter
はVCIとColliderが衝突したときに発生するイベントなので、レーザがスライドにあたった場合飲みを判定するようにif (item == "Lazer")
を条件分岐にいれています。さらにその中で右側にぶつかったのか左側にぶつかったのかを判定するためにhitがLeftScreen/RightScreenのどちらの値か? を判定しています。
function onTriggerEnter(item, hit)
if (item == "Lazer") then
if (hit == "LeftScreen") then
BoardStatus = "left"
print(string.format("onTriggerEnter: item=%s, item=%s", item, hit))
elseif (hit == "RightScreen") then
BoardStatus = "right"
print(string.format("onTriggerEnter: item=%s, item=%s", item, hit))
elseif (hit == "Board") then
local ms =
math.floor(
((tonumber(string.match(tostring(os.clock()), "%d%.(%d+)")) * 1000000) / 1000000 / 1000000) + 0.5
)
ParingId = os.date("%Y%m%d%H%M%S") .. ms
print(string.format("Slide Paring ID: %s", ParingId))
end
end
end
こちのスクリプトに対応するようにスライドVCI側に以下のようにRightScreen/LeftScreen, DummyRightScreen/DummyLeftScreenを作成します。これらは透明なオブジェクトで純粋に当たり判定をチェックするためだけに作っています。今回も親となるスライドと動きを連動させつつ別のサブアイテムとして判定させる必要があったのでDummyScreenを作ってスライドVCI側のスクリプトで対応付けています。
先ほどメッセージにbindさせたメソッドでは最終的にNextSlide
を呼びます。こちらではstatus
がright
なのかleft
なのかで挙動が変わるように条件分岐を入れてあります。
function NextSlide(boardStatus)
if boardStatus == "right" then
UseCount = UseCount + 1
if (UseCount > (MAX_SLIDE_PAGE - 1)) then
UseCount = 0
end
print("next")
elseif boardStatus == "left" then
UseCount = UseCount - 1
if (UseCount < 0) then
UseCount = (MAX_SLIDE_PAGE - 1)
end
print("prev")
end
local xidx = UseCount % MAX_SLIDE_X_INDEX
local yidx = math.ceil((UseCount + 1) / MAX_SLIDE_X_INDEX) % MAX_SLIDE_Y_INDEX
local width = (1.0 / MAX_SLIDE_X_INDEX)
local height = (1.0 / MAX_SLIDE_Y_INDEX)
local offset = Vector2.zero
offset.y = 1 - height * yidx
offset.x = width * xidx
local page = UseCount % (MAX_SLIDE_X_INDEX * MAX_SLIDE_Y_INDEX) + 1
vci.assets._ALL_SetMaterialTextureOffsetFromName("Slide", offset)
end
これでいったんレーザポインタでスライドを切り替える事が出来る用になりました。
スライドとレーザポインタのぺリング
実はこの時点ではスライドとレーザポインタのペアリングが出来ていないので複数のスライドを出しておくと、どれか一つにあたってる場合全部のスライドが同時に操作されてしまいます。
これはちょっと微妙なので、きちんとレーザポインタが当たってるスライドのページだけがめくれる形に変更します。
ペアリングの方式は色々ありそうです。まず考えたのはEmitWithId
を使いVciId
を送信することです。しかし、そもそもVciId
を特定の相手に伝える方法が良く分かりませんでした。
という分けで別案としてレーザとスライドがぶつかった際の時刻をペアリングIDとして使用することで擬似的な同期を作りました。レーザとスライドがぶつかった際にはonTriggerEnter
がどちらにも発生するはずです。ここでタイムスタンプを取れば概ね一致するはずですよね? これをIDとしてお互いを判別することでペアリングIDとしています。精度とか色々課題はある方式ですが、今回はそこまで厳密性はいらないので。
タイムスタンプの取得は以下のようなコードです。四捨五入で多少誤差を丸めています。
local ms =
math.floor(((tonumber(string.match(tostring(os.clock()), "%d%.(%d+)")) * 1000000) / 1000000 / 1000000) + 0.5)
ParingId = os.date("%Y%m%d%H%M%S") .. ms
レーザポインタ側からのメッセージの送信時は以下のようにparingId
も送信します。
function onUse()
vci.message.Emit("sendFromLazerPointer", {event = BoardStatus, paringId = ParingId})
end
また、スライド側でレーザポインタからメッセージを受け取った場合は以下のようにParingId
を比較し、スライド側とレーザポインタ側が同一であればNextSlide
を呼んでいます。
function OnLazerPointerMessage(sender, name, message)
print(string.format("sender ParingId:%s, own ParingId:%s, event:%s", message.paringId, ParingId, message.event))
if (message.paringId == ParingId) then
NextSlide(message.event)
end
end
vci.message.On("sendFromLazerPointer", OnLazerPointerMessage)
これで複数のスライドを出した状態でもレーザポインタを当てたスライドだけ切り替わる、という挙動を実現する事ができます。レーザポインタとスライドのスクリプトの全体は下記にあるので良ければそちらも参考にしてください。
まとめ
第一回、第二回、第三回と通してUnityのインストールの仕方からLuaスクリプトの作り方、そして複数のVCIの協調までを行いました。正直なところ、こうしたチュートリアルを書くことで私自身の理解を進める、という側面もあるので至らないところもあると思いますが、そこはご指摘いただければと。
Vキャスの大きな魅力はやはりVCIだと思います。アイテムがワールド側に結びついておらず、自由に持ち運べる事で様々なことができます。作成も比較的かんたんなので3Dは苦手ですが自分ももっと色々作ってみたいと思います。
Discussion