• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

ObserverMode and PlayerCameraData

Status
Not open for further replies.
One day, Craka_J and I were discussing ideas for Ardent Heroes. He said that it would be cool if you could go into "Observer Mode" when someone died, so that you could view another person's camera. I thought it was a pretty cool idea, but we weren't far enough in development so I never started that system.

I've seen this sort of system requested a few times over the years. It all arises from the issue that GetCameraField()/GetCameraTargetPositionX()/Y()/Z() are all local and return different values for different clients. It might return 100.00 for player 1, but for player 2 it might return 256, it really depends on where their camera is. Sadly, I never really looked into making the system, so I just told people that they would need to sync the values (and I mentioned that it would be slow). After doing some tests in DysfunctionaI's thread, I just decided I'd give it a go.

The main problem with syncing is that it is slow. You can't really get around that. Until Blizzard ports wc3 to Battle.net 2.0 (pls blizz), you can always expect average 250 - 400 ms on battle.net. LAN is a lot better, ~100-300 ms for me usually. But that doesn't mean that the system is not viable. I tested it, and it works fairly well on LAN. I decided that I might as well share it to (1) show the technique for those unfamiliar with it (2) in case someone might find it useful. Here is the code for the library that broadcasts PlayerCameraData:
JASS:
library PlayerCameraData
/**
*
*   EnableCameraBroadcast(player p, boolean flag)
*       - Enables/disables the storage (and syncing) of camera data for that player.
*   RegisterCameraDataUpdate(boolexpr b)
*       - Fires the boolexpr whenever the camera data is resynced.
*
*   PlayerCameraData.x[playerId] -> x-coordinate of camera for player 
*   PlayerCameraData.y[playerId] -> y-coordinate of camera for player 
*   PlayerCameraData.aoa[playerId] -> angle of attack for player
*   PlayerCameraData.rotation[playerId] -> rotation for player
*
*/
    globals
        private constant string CACHE_NAME = "PlayerCameraData"
        private constant string MISSION_PREFIX = "PCD_"
    
        private gamecache cache
        private player array playerStack
        private integer array playerIndex
        private integer stackIndex = 0
        private trigger syncTrigger = CreateTrigger()
        private trigger updateEvent = CreateTrigger()
    endglobals
    
    function RegisterCameraDataUpdate takes boolexpr b returns nothing 
        call TriggerAddCondition(updateEvent, b)
    endfunction
    
    private function SyncCameraData takes nothing returns nothing 
        local integer localStackIndex = stackIndex
        local integer i = localStackIndex
        local integer id
        local string  missionKey
        
        if i == 0 then
            return
        endif
        loop
            exitwhen i == 0
            
            set id = GetPlayerId(playerStack[i])
            set missionKey = MISSION_PREFIX + I2S(id)
            if GetLocalPlayer() == playerStack[i] then
                call StoreReal(cache, missionKey, "x", GetCameraTargetPositionX())
                call StoreReal(cache, missionKey, "y", GetCameraTargetPositionY())
                call StoreReal(cache, missionKey, "aoa", GetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK) * bj_RADTODEG)
                call StoreReal(cache, missionKey, "rot", GetCameraField(CAMERA_FIELD_ROTATION) * bj_RADTODEG)
                
                call SyncStoredReal(cache, missionKey, "x")
                call SyncStoredReal(cache, missionKey, "y")
                call SyncStoredReal(cache, missionKey, "aoa")
                call SyncStoredReal(cache, missionKey, "rot")
            endif
            
            set i = i - 1
        endloop
        call TriggerSyncReady()
        set i = localStackIndex
        loop 
            exitwhen i == 0
            set id = GetPlayerId(playerStack[i])
            set missionKey = MISSION_PREFIX + I2S(id)
            set PlayerCameraData.x[id] = GetStoredReal(cache, missionKey, "x")
            set PlayerCameraData.y[id] = GetStoredReal(cache, missionKey, "y")
            set PlayerCameraData.aoa[id] = GetStoredReal(cache, missionKey, "aoa")
            set PlayerCameraData.rotation[id] = GetStoredReal(cache, missionKey, "rot")
            set i = i - 1
        endloop
        call TriggerEvaluate(updateEvent)
        call TriggerExecute(syncTrigger)
    endfunction
    
    function EnableCameraBroadcast takes player p, boolean flag returns nothing 
        local integer id = GetPlayerId(p)
        if flag then
            if playerIndex[id] != 0 then 
                return
            endif
            set stackIndex = stackIndex + 1
            set playerStack[stackIndex] = p
            set playerIndex[id] = stackIndex
            if stackIndex == 1 then
                call TriggerExecute(syncTrigger)
            endif
        elseif playerIndex[id] != 0 then
            set playerStack[playerIndex[id]] = playerStack[stackIndex]
            set stackIndex = stackIndex - 1
            set playerIndex[id] = 0
        endif
    endfunction
    
    private module Init 
        private static method onInit takes nothing returns nothing 
            set cache = InitGameCache(CACHE_NAME)
            call TriggerAddAction(syncTrigger, function SyncCameraData)
        endmethod
    endmodule
    
    struct PlayerCameraData extends array
        static real array x
        static real array y
        static real array aoa
        static real array rotation
        
        implement Init
    endstruct
    
    function GetCameraTargetLocation takes player p returns location
        local integer id = GetPlayerId(p)
        return Location(PlayerCameraData.x[i], PlayerCameraData.y[i])
    endfunction

endlibrary

Demo "ObserverMode":
JASS:
library ObserverMode initializer Init requires PlayerCameraData 

    globals
        integer currentlyViewing = -1
        
        constant real PAN_DURATION = 0.25
    endglobals

    private function Esc takes nothing returns nothing 
        local player p = GetTriggerPlayer()
        if p == Player(0) then
            if currentlyViewing == 1 then
                call BJDebugMsg("|cffffcc00Player 1|r is no longer viewing |cffffcc00Player 2's|r camera.")
                set currentlyViewing = -1
            else
                call BJDebugMsg("|cffffcc00Player 1|r is now viewing |cffffcc00Player 2's|r camera.")
                set currentlyViewing = 1
            endif
        elseif p == Player(1) then
            if currentlyViewing == 0 then
                call BJDebugMsg("|cffffcc00Player 2|r is no longer viewing |cffffcc00Player 1's|r camera.")
                set currentlyViewing = -1
            else
                call BJDebugMsg("|cffffcc00Player 2|r is now viewing |cffffcc00Player 1's|r camera.")
                set currentlyViewing = 0
            endif
        endif
    endfunction
    
    private function OnCameraDataUpdate takes nothing returns boolean
        local integer viewer = 0
        if currentlyViewing == -1 then
            return false
        endif
        if currentlyViewing == 0 then
            set viewer = 1
        endif
        if GetLocalPlayer() == Player(viewer) then
            call PanCameraToTimed(PlayerCameraData.x[currentlyViewing], PlayerCameraData.y[currentlyViewing], PAN_DURATION)
            call SetCameraField(CAMERA_FIELD_ANGLE_OF_ATTACK, PlayerCameraData.aoa[currentlyViewing], PAN_DURATION)
            call SetCameraField(CAMERA_FIELD_ROTATION, PlayerCameraData.rotation[currentlyViewing], PAN_DURATION)
        endif
        return false 
    endfunction
    
    private function GameStart takes nothing returns nothing 
        call BJDebugMsg("Press |cffffcc00ESC|r to view the other player's camera.")
        call EnableCameraBroadcast(Player(0), true)
        call EnableCameraBroadcast(Player(1), true)
        call RegisterCameraDataUpdate(Condition(function OnCameraDataUpdate))
    endfunction

    private function Init takes nothing returns nothing 
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
        call TriggerRegisterPlayerEvent(t, Player(1), EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddAction(t, function Esc)
        
        call TimerStart(CreateTimer(), 0, false, function GameStart)
    endfunction
endlibrary

I'll clean the code and add comments a little later. And maybe I'll make ObserverMode into an actual system. Right now it is just a demo. To test it, download the map, put it in your maps folder, open JNGP 2.0.X. to start the multiplayer emulation, load it on LAN and then press ESC on either computer side. The rest should be self explanatory. As one client moves the camera, the other client's camera should move as well (with a slight delay, depending on your connection speeds). You can also test on two separate computers like I did.

Right now, you first have to call EnableCameraBroadcast for the players you want to get global camera data for. This will let the system know that the player's data should be sync'd. The data is sync'd in batches -> it stores the current camera data in gamecache -> syncs it -> waits for syncing -> loads data and stores it in the struct -> repeat. Theoretically, maybe I could capture more camera movements by doing several batch syncs (e.g. sync every 0.1 seconds instead of after each sync is finished). It would require tweaking so that the gamecache data wouldn't get overwritten, but it could work. Although, it is not bad in its current state.

Credits to Xarian for some info on sync'ing. Enjoy, and hopefully someone finds a use for this. It could be really cool for altered melee/AoS maps. Basically any map with free cameras. Do not use this for RPG/fps/custom cameras, that is just nonsense--you already have control over where the camera is.
 

Attachments

  • FollowCamera.w3x
    19.5 KB · Views: 177
Last edited:
I could make it use Network. This is just a prototype and was half made for testing purposes. :)

Although, Network syncs integers iirc (right?). I can cast them (R2I), I suppose, since decimal precision is kind of insignificant with cameras.

As for the key names, that is as simple as setting MISSION_PREFIX to "" and setting "aoa" to "a" and "rot" to "r". Tbh, the only motivation to use Network is to make it usable with Network, which is fine. My point is that it is a matter of compliance, not a matter of speed.
 
Status
Not open for further replies.
Top