====== 飲み物VCI(わんコメ対応) ====== **[[deliverytool/onecomme/plugin]]**でYouTube・ニコニコ生放送のコメントを受信してアクションを起こすVCIのサンプルです。 * 「コーヒー」「紅茶」「ジュース」のいずれかの文字列を含むコメントを受信すると、該当するドリンクが落下する * 飲み物は頭に近づけると、中身が減る * [[https://developer.virtualcast.jp/vci-docs/manual/osc/onecomme_osc/index.html|プラグインの仕様ドキュメント]] \\ === サンプルデータ === https://virtualcast.jp/products/48ebd27dd1b65229dc671ed76874150f69eaa4aa22b8f5114d1d66e184579d3e === Unitypackage === {{ vci:sample:oscapi:onecomme:drinkvci.unitypackage |}} ===== コンポーネント設定 ===== VCI Object {{:vci:sample:oscapi:onecomme:drink1.png?direct}} --> Sub Item 設定一覧 (クリックで展開) # VCI Sub Item : TeaDummy1\\ {{:vci:sample:oscapi:onecomme:drink2.png?direct}} VCI Sub Item : CoffeeDummy1\\ {{:vci:sample:oscapi:onecomme:drink3.png?direct}} VCI Sub Item : JuiceDummy1\\ {{:vci:sample:oscapi:onecomme:drink4.png?direct}} 各2個目以上のSub Item省略 <-- ===== VCIスクリプト ===== local debagFlag = false local roomFlag local camera local myId local dropHeight = 2 --ドリンクを落下させる高さ。プレイヤーの足元からの高さ local dropDis = 0.3 --ドリンクを落下させる距離。プレイヤーの中心からの距離 local intervalTime = 10 --ドリンクを落下させる際のインターバル。 local drinkKey = {"紅茶","コーヒー","ジュース"} local limit = {3,3,4} --ドリンクそれぞれの数 local objName = {"TeaDummy","CoffeeDummy","JuiceDummy"} --提供の状態の記録(3次元配列) ---1次元目:ドリンクの種類(紅茶、コーヒー、ジュース) ---2次元目:提供記録(提供された順に記録、1が1番新しい) ---3次元目:提供指令の記録(オブジェクトのナンバリング、提供指令のキュー、提供指令発令の時間) ---を示す。 local supplyTbl = {} --各ドリンクの状態を記録する変数 local drink = {} local drinkCheckTime = {} local grabLocalFlag = {} for i = 1, #limit do supplyTbl[i] = {} drink[i] = {} drinkCheckTime[i] = {} grabLocalFlag[i] = {} for j = 1, limit[i] do drink[i][j] = vci.assets.GetTransform(objName[i]..j) drinkCheckTime[i][j] = 0 grabLocalFlag[i][j] = false end end local drinkState = {} local childObjName = {"/TeaCup/Tea", "/CoffeeCup/Coffee", "/Juice/Juice"} for i = 1, #limit do drinkState[i] = {} for j = 1, limit[i] do drinkState[i][j] = 0 end end local drinkDis = 0.20 --飲む判定をする際の距離 local drinkInterval = 1.5 --飲む判定のインターバル --飲み物落下対象者チェック --カメラが出てない場合、ランダムに対象を選ぶ --カメラがある場合、カメラに映っており、カメラに近い人を対象。 --カメラがあっても、カメラに映っている人がいなければ、ランダム対象。 local function targetCheck() local players = vci.vc.room.GetAllPlayers() local tempFlag = false --VCIオーナーがいるかどうかのチェック for i = 1, #players do if players[i].GetId() == vci.state.Get("OwnerID") then tempFlag = true end end local tgt local randomFlag = true if tempFlag then camera = vci.vc.room.streamCamera.GetLocalStreamCameraByPlayerId(vci.state.Get("OwnerID")) if camera.IsAvailable() then local cPos = camera.GetPosition() local ang = camera.GetFov()/2 local cIRot = Quaternion.Inverse(camera.GetRotation()) for i = 1, #players do if players[i].Character.IsAvailable() then if players[i].Character.GetBoneTransform("Head") ~= nil then local pPos = players[i].Character.GetBoneTransform("Head").position local dis = pPos - cPos local cIDis = cIRot*dis if cIDis.z > 0 and cIDis.z*math.tan(ang*math.pi/180) > math.abs(cIDis.y) and 16/9*cIDis.z*math.tan(ang*math.pi/180) > math.abs(cIDis.x) then if tgt == nil then tgt = i randomFlag = false else local dis1 = Vector3.Distance(cPos, players[tgt].Character.GetBoneTransform("Head").position) local dis2 = Vector3.Distance(cPos, players[i].Character.GetBoneTransform("Head").position) if dis1 > dis2 then tgt = i end end end end end end end end if tgt == nil then if #players == 1 then tgt = 1 else tgt = math.random(1, #players) end else --print("Target : "..players[tgt].GetName()) end return tgt, randomFlag end --落下用stateがtrueで、対象ドリンクの所有権があれば、落下処理を実行 --落下位置は、ランダム対象の場合、目の前30㎝ --ランダムでない場合、カメラの向き30㎝の位置に落下 --カメラに映っている人がいるか、いないかで、処理が変わる --処理が終わったら、stateを更新 local function drinkDrop() for i = 1, #limit do for j = 1, limit[i] do if vci.state.Get("DropDrink"..i..j) ~= nil then if vci.state.Get("DropDrink"..i..j) and drink[i][j].IsMine then local targetNum, randomFlag = targetCheck() local targetPlayer = vci.vc.room.GetAllPlayers()[targetNum] local dropPos if randomFlag then dropPos = targetPlayer.GetPosition() + targetPlayer.GetForward()*dropDis else local dis = camera.GetPosition() - targetPlayer.GetPosition() dis.y = 0 dropPos = targetPlayer.GetPosition() + dis.normalized*dropDis end if targetPlayer.Character.GetBoneTransform("Head") ~= nil then dropPos.y = targetPlayer.Character.GetBoneTransform("Head").position.y + dropHeight end drink[i][j]._ALL_SetActive(true) drink[i][j].SetPosition(dropPos) --vci.assets.GetTransform("SoundObj").GetAudioSources()[1]._ALL_PlayOneShot(0.5) print("ドリンク落下SE") vci.state.Set("DropDrink"..i..j, false) vci.state.Set("DrinkState"..i..j, 1) vci.state.Set("FixDrink"..i..j, "none") end end end end end --ドリンクの残量の更新 --stateをチェックして、更新があれば、更新を実行 local function drinkStateUpdate() local players = vci.vc.room.GetAllPlayers() for i = 1, #limit do for j = 1, limit[i] do if vci.state.Get("DrinkState"..i..j) ~= nil then if drink[i][j].IsMine then for k = 1, #players do if players[k].Character.IsAvailable() then if players[k].Character.GetBoneTransform("Head") ~= nil then local dis = Vector3.Distance(players[k].Character.GetBoneTransform("Head").position, drink[i][j].GetPosition()) if dis < drinkDis then local stateNum = vci.state.Get("DrinkState"..i..j) if stateNum ~= 4 and os.time(sec) - drinkCheckTime[i][j] > drinkInterval then vci.state.Set("DrinkState"..i..j, stateNum+1) drinkCheckTime[i][j] = os.time(sec) end end end end end end if drinkState[i][j] ~= tonumber(vci.state.Get("DrinkState"..i..j)) then drinkState[i][j] = tonumber(vci.state.Get("DrinkState"..i..j)) for k = 1, 3 do local tempFlag = false if k == drinkState[i][j] then tempFlag = true end vci.assets.GetTransform("/"..objName[i]..j..childObjName[i]..k).SetActive(tempFlag) end end end end end end --ドリンクの固定処理 local function drinkFix() for i = 1, #objName do for j = 1, limit[i] do if vci.state.Get("FixDrink"..i..j) ~= nil then if not(vci.state.Get("GrabFlag"..i..j)) and vci.state.Get("FixDrink"..i..j) ~= "none" and drink[i][j].IsMine then local tbl = vci.state.Get("FixDrink"..i..j) local pos = Vector3.__new(tonumber(tbl[1]), tonumber(tbl[2]), tonumber(tbl[3])) local rot = Quaternion.__new(tonumber(tbl[4]), tonumber(tbl[5]), tonumber(tbl[6]), tonumber(tbl[7])) drink[i][j].SetPosition(pos) drink[i][j].SetRotation(rot) end end end end end --Transformの桁を丸める処理 local function transCompress(pos, rot) local tbl = {} tbl[1] = tostring(0.001*math.floor(pos.x*1000+0.5)) tbl[2] = tostring(0.001*math.floor(pos.y*1000+0.5)) tbl[3] = tostring(0.001*math.floor(pos.z*1000+0.5)) tbl[4] = tostring(0.0001*math.floor(rot.x*10000+0.5)) tbl[5] = tostring(0.0001*math.floor(rot.y*10000+0.5)) tbl[6] = tostring(0.0001*math.floor(rot.z*10000+0.5)) tbl[7] = tostring(0.0001*math.floor(rot.w*10000+0.5)) return tbl end local youtubeAdress1 = "/vc-official/onecomme/youtube/comment" local youtubeAdress2 = "/vc-official/onecomme/youtube/super" local niconicoAdress1 = "/vc-official/onecomme/niconico/comment" local niconicoAdress2 = "/vc-official/onecomme/niconico/gift" --対象の飲み物コメントが来た際に、飲み物を落下させる処理 --飲み物をすべて提供していない時には、順番に提供。(stateに書き込み) --すべて提供した後は、Grab判定(state)、提供処理中判定(state経由で更新)、インターバル判定 --をチェックして、提供処理を行う。(stateへの書き込み) local function wankomeDrink(content) if roomFlag then if vci.state.Get("OwnerID") == myId then local oscData = json.parse(content).comment for i = 1, #limit do if string.find(oscData, drinkKey[i]) ~= nil then if #supplyTbl[i] < limit[i] then local num = #supplyTbl[i]+1 table.insert(supplyTbl[i], 1, {num, true, os.time(sec)}) vci.state.Set("SupplyTbl", supplyTbl) vci.state.Set("DropDrink"..i..num, true) else for j = limit[i], 1, -1 do if not(vci.state.Get("GrabFlag"..i..supplyTbl[i][j][1])) and not(supplyTbl[i][j][2]) and os.time(sec)-supplyTbl[i][j][3] >= intervalTime then local num = supplyTbl[i][j][1] table.remove(supplyTbl[i], j) table.insert(supplyTbl[i], 1, {num, true, os.time(sec)}) vci.state.Set("SupplyTbl", supplyTbl) vci.state.Set("DropDrink"..i..num, true) break end end end end end end end end vci.osc.RegisterMethod(youtubeAdress1, wankomeDrink, {ExportOscType.BlobAsUtf8}) vci.osc.RegisterMethod(youtubeAdress2, wankomeDrink, {ExportOscType.BlobAsUtf8}) vci.osc.RegisterMethod(niconicoAdress1, wankomeDrink, {ExportOscType.BlobAsUtf8}) --vci.osc.RegisterMethod(niconicoAdress2, wankomeHakushu, {ExportOscType.BlobAsUtf8}) --初期化処理 local function initialize() --ルームの識別 if vci.vc.GetSpaceType() == ExportVcSpaceType.studio then roomFlag = false else roomFlag = true myId = vci.vc.room.GetLocalPlayer().GetId() end if roomFlag then if vci.assets.IsMine then --VCI生成時のstate初期化と --VCIオーナーでローカル管理する変数の読み込み if vci.state.Get("OwnerID") == nil then vci.state.Set("OwnerID", vci.vc.room.GetLocalPlayer().GetId())--VCIオーナー識別用 vci.state.Set("SupplyTbl", supplyTbl)--ドリンクアクティブ識別用 for i = 1, #limit do for j = 1, limit[i] do vci.state.Set("GrabFlag"..i..j, false)--グラブ状態共有用 vci.state.Set("DropDrink"..i..j, false)--ドリンク落下処理実行トリガー vci.state.Set("DrinkState"..i..j, 1)--ドリンクの残量状態 vci.state.Set("FixDrink"..i..j, "none") end end elseif myId == vci.state.Get("OwnerID") then supplyTbl = vci.state.Get("SupplyTbl") end --デバッグ時にローカルストリーミングカメラを自動生成 if debagFlag then if vci.state.Get("OwnerID") == vci.vc.room.GetLocalPlayer().GetId() then camera = vci.vc.room.streamCamera.GetMyLocalStreamCamera() if not camera.IsAvailable() and vci.vc.room.GetLocalPlayer().Character.IsAvailable() then local pos = vci.vc.room.GetLocalPlayer().GetPosition() + vci.vc.room.GetLocalPlayer().GetForward() local headPos = vci.vc.room.GetLocalPlayer().Character.GetBoneTransform("Head").position pos.y = headPos.y local rot = Quaternion.LookRotation(headPos - pos, Vector3.up) vci.vc.room.streamCamera.CreateMyLocalStreamCamera(pos, rot, function(result) print("CreateMyLocalStreamCamera: " .. tostring(result.isSuccess)) end) end end end end --飲み物のアクティブ状態の更新 for i = 1, #limit do for j = 1, limit[i] do drink[i][j].SetActive(false) end end if vci.state.Get("SupplyTbl") ~= nil then for i = 1, #limit do for j = 1, #vci.state.Get("SupplyTbl")[i] do local num = vci.state.Get("SupplyTbl")[i][j][1] drink[i][num].SetActive(true) end end end end end initialize() --飲み物のドロップ処理がされた場合(state管理) --VCIオーナーのローカル変数を更新 function update() if roomFlag and vci.state.Get("OwnerID") == myId then for i = 1, #limit do if #supplyTbl[i] == limit[i] then for j = 1, limit[i] do if not(vci.state.Get("DropDrink"..i..j)) and supplyTbl[i][j][2] then supplyTbl[i][j][2] = false vci.state.Set("SupplyTbl", supplyTbl) end end end end end end function updateAll() for i = 1, #limit do for j = 1, limit[i] do --Grabで奪われた際の、stateの書き込み前後対策 if grabLocalFlag[i][j] and not(vci.state.Get("GrabFlag"..i..j)) then vci.state.Set("GrabFlag"..i..j, true) end --Grabした状態で落ちた際の、state情報修正 if drink[i][j].IsMine and not(grabLocalFlag[i][j]) and vci.state.Get("GrabFlag"..i..j) then vci.state.Set("GrabFlag"..i..j, false) end end end --飲み物落下処理 drinkDrop() --ドリンクの残量の更新 drinkStateUpdate() --ドリンクの固定処理 drinkFix() end --Grabした際に、StateにGrab状態を書き込み --Grabされている飲み物は、コメントで落下されないように --また、固定状態の解除 function onGrab(item) for i = 1, #objName do if string.sub(item, 1, -2) == objName[i] then local num = tonumber(string.sub(item, -1)) grabLocalFlag[i][num] = true vci.state.Set("GrabFlag"..i..num, true) vci.state.Set("FixDrink"..i..num, "none") end end end --UnGrabした際に、StateにunGrab状態を書き込み function onUngrab(item) for i = 1, #objName do if string.sub(item, 1, -2) == objName[i] then local num = tonumber(string.sub(item, -1)) grabLocalFlag[i][num] = false vci.state.Set("GrabFlag"..i..num, false) if vci.state.Get("FixDrink"..i..num) ~= "none" then local pos = drink[i][num].GetPosition() local rot = drink[i][num].GetRotation() local tbl = transCompress(pos, rot) vci.state.Set("FixDrink"..i..num, tbl) end end end end --ドリンクをUseした際に固定状態に切り替え function onUse(use) for i = 1, #objName do if string.sub(use, 1, -2) == objName[i] then local num = tonumber(string.sub(use, -1)) local pos = drink[i][num].GetPosition() local rot = drink[i][num].GetRotation() local tbl = transCompress(pos, rot) vci.state.Set("FixDrink"..i..num, tbl) end end end