目次

飲み物VCI(わんコメ対応)

VirtualCast公式わんコメOSCプラグインでYouTube・ニコニコ生放送のコメントを受信してアクションを起こすVCIのサンプルです。


サンプルデータ

https://virtualcast.jp/products/48ebd27dd1b65229dc671ed76874150f69eaa4aa22b8f5114d1d66e184579d3e

Unitypackage

drinkvci.unitypackage

コンポーネント設定

VCI Object

Sub Item 設定一覧 (クリックで展開)
VCI Sub Item : TeaDummy1

VCI Sub Item : CoffeeDummy1

VCI Sub Item : JuiceDummy1

各2個目以上のSub Item省略

VCIスクリプト

main.lua
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