- Joined
- Jan 30, 2020
- Messages
- 775
First off: This is might require some code reading to solve, I think, and possibly a fair amount of it.
I hope that someone can help me.
The Issue
My map shoots projectiles towards the mouse-location and it seems to work fine in single player.
When I go multiplayer (or technically always, I guess), it uses the location of the player who moved its mouse last for all players, resulting in hard-to-impossible to do what you expect in the correct direction for all players.
The details
The map is Mecha Protectors I use [vJASS] - [Snippet] Mouse Utility. There is only 1 unit (per player) for players and you have locked camera and cannot deselect it. There is a dash-ability.
With the Mouse utility (or blizzards API actually), it doesn't fire updates unless you move the mouse.
So if you hold your mouse close to your hero, let go of the mouse and dash (towards the mouse location), you would dash in that direction as expected. However, if you would dash again (still haven't touched the mouse), you would dash backwards (towards the last registered world-location) instead of the same direction again (as the mouse-location suggests).
As an image, the same thing:
You start at blue circle 1 with mouse-location at green square 1, if you would shoot, you'd fire in the red-direction, if you would dash to blue circle 2, the mouse world location would be green 2, but you would fire in the red direction, not towards the green 2, because you haven't fired any mouse-move-events!
The solution:
Keep track of the relative position and when you need the mouse-location, do a new relative-to-world calculation:
Of course, I have had prints that ensures that
At start-up register the unit and the onMouseMovement-function to the MouseUtil, track the unit-to-mouse location. When you want the world-position, just add the unit-location to the unit-to-mouse location and BANG, you have it!
Except for this multiplayer issue.
Other, possibly relevant scripts are the custom action system, that requests the mouse-position:
Heroes can have "targets" selected. When firing an attack, you fire towards the target (if you have one) and towards mouse-location otherwise.
The idea is that different heroes can have different abilities on different keys.
In the map (linked), the
Abilities can be found in
I have not been able to replicate this using 2 instances on the same machine, because when alt+tabing to a WC3 instance, you instantly fire a mouse-event when entering the game, so it always "works" when playing LAN on the same computer, because the WC3 instance you're playing is the one that had its mouse-event fire last...
Sorry for the wall of text, but it isn't all that trivial to describe...
I hope that someone can help me.
The Issue
My map shoots projectiles towards the mouse-location and it seems to work fine in single player.
When I go multiplayer (or technically always, I guess), it uses the location of the player who moved its mouse last for all players, resulting in hard-to-impossible to do what you expect in the correct direction for all players.
The details
The map is Mecha Protectors I use [vJASS] - [Snippet] Mouse Utility. There is only 1 unit (per player) for players and you have locked camera and cannot deselect it. There is a dash-ability.
With the Mouse utility (or blizzards API actually), it doesn't fire updates unless you move the mouse.
So if you hold your mouse close to your hero, let go of the mouse and dash (towards the mouse location), you would dash in that direction as expected. However, if you would dash again (still haven't touched the mouse), you would dash backwards (towards the last registered world-location) instead of the same direction again (as the mouse-location suggests).
As an image, the same thing:
You start at blue circle 1 with mouse-location at green square 1, if you would shoot, you'd fire in the red-direction, if you would dash to blue circle 2, the mouse world location would be green 2, but you would fire in the red direction, not towards the green 2, because you haven't fired any mouse-move-events!
The solution:
Keep track of the relative position and when you need the mouse-location, do a new relative-to-world calculation:
Of course, I have had prints that ensures that
this
and player number
values are as expected here.
JASS:
library MouseUtilsLockCamExtension requires MouseUtils
struct LockCamExtension extends array
private unit u
private real relativeX
private real relativeY
static method get takes player p returns thistype
return thistype(GetPlayerId(p))
endmethod
static method onMouseMovement takes nothing returns nothing
local player p = GetTriggerPlayer()
local thistype this = get(p)
set .relativeX = GetPlayerMouseX(p) - GetUnitX(u)
set .relativeY = GetPlayerMouseY(p) - GetUnitY(u)
//call BJDebugMsg("OnMouseMovement: " + I2S(GetPlayerId(p)) + ", this=" + I2S(this) + ", x=" + R2S(.relativeX))
set p = null
endmethod
static method registerUnit takes unit u returns nothing
local thistype this = get(GetOwningPlayer(u))
//call BJDebugMsg("Registering for: " + I2S(GetPlayerId(GetOwningPlayer(u))) + ", " + GetUnitName(u))
set .u = u
endmethod
method mx takes nothing returns real
return GetUnitX(.u) + .relativeX
endmethod
method my takes nothing returns real
return GetUnitY(.u) + .relativeY
endmethod
endstruct
function registerLockCamUnit takes unit u returns nothing
call LockCamExtension.registerUnit(u)
endfunction
function initLockCamExtension takes nothing returns nothing
call UserMouse.registerCode(function LockCamExtension.onMouseMovement, EVENT_MOUSE_MOVE)
endfunction
function GetMouseX takes player p returns real
//call BJDebugMsg("GetMouseX: " + I2S(GetPlayerId(p)))
return LockCamExtension.get(p).mx()
endfunction
function GetMouseY takes player p returns real
return LockCamExtension.get(p).my()
endfunction
endlibrary
Except for this multiplayer issue.
Other, possibly relevant scripts are the custom action system, that requests the mouse-position:
JASS:
library MetroidvaniaController initializer onInit requires MouseUtilsLockCamExtension, MetroidvaniaUtils, MetroidvaniaUi
globals
constant oskeytype MC_KEY_ATTACK = OSKEY_Q
constant oskeytype MC_KEY_ATTACK_ALT = OSKEY_W
constant oskeytype MC_KEY_UTIL_1 = OSKEY_E
constant oskeytype MC_KEY_UTIL_2 = OSKEY_R
constant oskeytype MC_KEY_DASH = OSKEY_D
constant oskeytype MC_KEY_INTERACT = OSKEY_F
constant oskeytype MC_KEY_MAP = OSKEY_M
constant integer NO_ACTION = -1
constant integer ACITON_STATE_READY = 1
constant integer ACTION_STATE_DOING = 2
constant integer ACTION_STATE_BACKSWING = 3
constant integer ACTION_STATE_COOLDOWN = 4
//constant integer UI_ATTACK = 0
//constant integer UI_ATTACK_ALT = 1
//constant integer UI_UTIL_1 = 2
//constant integer UI_UTIL_2 = 3
//constant integer UI_DASH = 4
//constant integer UI_PASSIVE_1 = 5
//constant integer UI_PASSIVE_2 = 6
//constant integer UI_INTERACT = 7
//constant integer UI_MAP = 8
endglobals
private keyword controllerCallback
private interface ActionInterface
method onStart takes nothing returns nothing defaults nothing
method onActionPoint takes nothing returns nothing defaults nothing
method onBackswing takes nothing returns nothing defaults nothing
method onRelease takes nothing returns nothing defaults nothing
method onCooldownFinished takes nothing returns nothing defaults nothing
method animationSpeed takes nothing returns real defaults 1.0
method abilityText takes nothing returns string defaults "TODO"
method abilityName takes nothing returns string defaults "TODO"
//method updateCooldown takes nothing returns nothing defaults nothing
endinterface
struct CooldownDetails extends array
implement Alloc
readonly real actionPoint
readonly real actionDuration
readonly real actionCooldown
public static method create takes real actionPoint, real actionDuration, real actionCooldown returns thistype
local thistype this = thistype.allocate()
set .actionPoint = actionPoint
set .actionDuration = actionDuration
set .actionCooldown = actionCooldown
return this
endmethod
public method printDetails takes nothing returns nothing
call BJDebugMsg("this=" + I2S(this) + ", AP=" + R2S(actionPoint) + ", Dur="+R2S(actionDuration) + ", Cooldown=" + R2S(actionCooldown))
endmethod
public method update takes real actionPoint, real actionDuration, real actionCooldown returns nothing
set .actionPoint = actionPoint
set .actionDuration = actionDuration
set .actionCooldown = actionCooldown
//call BJDebugMsg("--- Updated cooldown details ---")
//call printDetails()
endmethod
public method destroy takes nothing returns nothing
call this.deallocate()
endmethod
endstruct
private module ActionFlow
private static method releaseTimer takes nothing returns nothing
call ReleaseTimer(GetExpiredTimer())
endmethod
private static method updateUi takes nothing returns nothing
local thistype this = GetTimerData(GetExpiredTimer())
local real remainingCooldown = TimerGetRemaining(.cooldownTimer)
if remainingCooldown > 0 then
if GetLocalPlayer() == GetOwningPlayer(u) then
call updateCooldown(uiButtonIndex, .cooldownDetails.actionCooldown, remainingCooldown)
endif
else
call ReleaseTimer(GetExpiredTimer())
set .cooldownTimer = null
endif
endmethod
private static method cooldownFinished takes nothing returns nothing
local timer t = GetExpiredTimer()
local thistype this = GetTimerData(t)
if .onCooldownFinished.exists then
call .onCooldownFinished()
endif
call ReleaseTimer(t)
set t = null
set actionState = ACITON_STATE_READY
call controllerCallback.evaluate(u, this)
endmethod
private static method atBackswing takes nothing returns nothing
local timer t = GetExpiredTimer()
local thistype this = GetTimerData(t)
if .onBackswing.exists then
call .onBackswing()
endif
call TimerStart(t, cooldownDetails.actionCooldown - cooldownDetails.actionDuration, false, function thistype.cooldownFinished)
set t = null
set actionState = ACTION_STATE_COOLDOWN
call controllerCallback.evaluate(u, this)
endmethod
private static method atActionPoint takes nothing returns nothing
local timer t = GetExpiredTimer()
local thistype this = GetTimerData(t)
if .onActionPoint.exists then
call .onActionPoint()
endif
call TimerStart(t, cooldownDetails.actionDuration - cooldownDetails.actionPoint, false, function thistype.atBackswing)
set t = null
if GetLocalPlayer() == GetOwningPlayer(u) then
call SelectUnit(u, true)
endif
set actionState = ACTION_STATE_BACKSWING
call controllerCallback.evaluate(u, this)
endmethod
method start takes nothing returns nothing
call ConsumeMana(u, energyCost)
if .onStart.exists then
call .onStart()
endif
set .cooldownTimer = NewTimerEx(this)
call TimerStart(cooldownTimer, cooldownDetails.actionCooldown, false, function thistype.releaseTimer)
call TimerStart(NewTimerEx(this), cooldownDetails.actionPoint, false, function thistype.atActionPoint)
call TimerStart(NewTimerEx(this), 0.02, true, function thistype.updateUi)
set actionState = ACTION_STATE_DOING
if cooldownDetails.actionPoint == 0. then
call controllerCallback.evaluate(u, this)
elseif GetLocalPlayer() == GetOwningPlayer(u) then
call ClearSelection()
endif
endmethod
endmodule
struct HeroActionBase extends ActionInterface
private integer actionState
public boolean isHolding
readonly timer cooldownTimer
readonly unit u
readonly CooldownDetails cooldownDetails
readonly integer uiButtonIndex
real energyCost
boolean ignoreTarget
public method isDoing takes nothing returns boolean
return actionState == ACTION_STATE_DOING
endmethod
public method isBackswing takes nothing returns boolean
return actionState == ACTION_STATE_BACKSWING
endmethod
public method isOnCooldown takes nothing returns boolean
return actionState != ACITON_STATE_READY
endmethod
public method isReady takes nothing returns boolean
return actionState == ACITON_STATE_READY
endmethod
public method isCastable takes nothing returns boolean
return isReady() and GetMana(u) > energyCost
endmethod
method operator owner takes nothing returns player
return GetOwningPlayer(u)
endmethod
stub method updateAbilityDetails takes nothing returns nothing
call BJDebugMsg("!!! NO UPDATE FUNCTION IMPLEMENTED !!!")
endmethod
implement ActionFlow
public static method create takes unit u, integer uiIndex, string iconPath returns thistype
local thistype this = thistype.allocate()
set .u = u
set .cooldownDetails = CooldownDetails.create(1.0, 1.0, 1.0)
set .isHolding = false
set .actionState = ACITON_STATE_READY
set .uiButtonIndex = uiIndex
call BlzFrameSetVisible(ui_frame[uiButtonIndex + UI_ABIL_BTN_OFFSET], true)
call BlzFrameSetVisible(ui_frame[uiButtonIndex + UI_ABIL_ICON_OFFSET], true)
call BlzFrameSetTexture(ui_frame[uiButtonIndex + UI_ABIL_ICON_OFFSET], iconPath, 0, true)
call BlzFrameSetValue(ui_frame[uiButtonIndex + UI_ABIL_COOLDOWN_OFFSET], 0)
return this
endmethod
public method destroy takes nothing returns nothing
if cooldownTimer != null then
call ReleaseTimer(cooldownTimer)
set cooldownTimer = null
endif
call cooldownDetails.destroy()
call this.deallocate()
endmethod
endstruct
struct MetroidvaniaController extends array
private static trigger mc_main_down
private static trigger mc_main_up
private static trigger mc_select
private static trigger mc_deselect
private unit u
private unit target
private boolean isHolding
private timer t
private boolean isDoingAction
private HeroActionBase currentAction
private HeroActionBase queuedAction
private real currentActionTargetX
private real currentActionTargetY
private HeroActionBase attack
private HeroActionBase attackAlt
private HeroActionBase util1
private HeroActionBase util2
private HeroActionBase dash
private HeroActionBase interact
private HeroActionBase map
private method keyToAction takes oskeytype key returns HeroActionBase
if key == MC_KEY_ATTACK then
return attack
elseif key == MC_KEY_ATTACK_ALT then
return attackAlt
elseif key == MC_KEY_UTIL_1 then
return util1
elseif key == MC_KEY_UTIL_2 then
return util2
elseif key == MC_KEY_DASH then
return dash
elseif key == MC_KEY_INTERACT then
return interact
elseif key == MC_KEY_MAP then
return map
else
return NO_ACTION
endif
endmethod
private method updateTargetCoords takes HeroActionBase action returns nothing
if target != null and UnitAlive(target) and not action.ignoreTarget then
set .currentActionTargetX = GetUnitX(target)
set .currentActionTargetY = GetUnitY(target)
else
//call BJDebugMsg("updateTargetCoords: Playerid=" + I2S(GetPlayerId(GetOwningPlayer(.u))))
set .currentActionTargetX = GetMouseX(GetOwningPlayer(.u))
set .currentActionTargetY = GetMouseY(GetOwningPlayer(.u))
endif
endmethod
public method tryToExecuteAction takes HeroActionBase action returns nothing
if not isDoingAction and action > 0 and action.isCastable() then
set currentAction = action
set isDoingAction = true
call updateTargetCoords(action)
call action.start()
endif
endmethod
static method controllerCallback takes unit u, HeroActionBase action returns nothing
local thistype this = thistype(GetPlayerId(GetOwningPlayer(u)))
//Should be at start of "backswing" at earliest, allow queueing actions at start of "backswing"
set isDoingAction = false
if currentAction == action then
if currentAction.isHolding and currentAction.isCastable() then
call updateTargetCoords(currentAction)
call currentAction.start()
elseif queuedAction != NO_ACTION and queuedAction.isCastable() then
call updateTargetCoords(queuedAction)
set currentAction = queuedAction
call currentAction.start()
endif
endif
endmethod
private static method keyPressed takes nothing returns nothing
local thistype this = thistype(GetPlayerId(GetTriggerPlayer()))
local real tx
local real ty
local HeroActionBase action
if UnitAlive(u) then
if not isDoingAction then
set currentAction = keyToAction(BlzGetTriggerPlayerKey())
set queuedAction = NO_ACTION
if currentAction != NO_ACTION then
//call BJDebugMsg("Trying to cast: " + currentAction.abilityName())
set currentAction.isHolding = true
call tryToExecuteAction(currentAction)
else
//call BJDebugMsg("Action is NO_ACTION")
endif
else
//call BJDebugMsg("Doing Action...")
set action = keyToAction(BlzGetTriggerPlayerKey())
if currentAction == action then
set currentAction.isHolding = true
else
set queuedAction = action
endif
endif
else
set currentAction = NO_ACTION
set queuedAction = NO_ACTION
endif
endmethod
private static method keyReleased takes nothing returns nothing
local thistype this = thistype(GetPlayerId(GetTriggerPlayer()))
local HeroActionBase action = keyToAction(BlzGetTriggerPlayerKey())
if action != NO_ACTION then
set action.isHolding = false
call action.onRelease()
endif
endmethod
private static method selectTargetNearMouse takes nothing returns nothing
local player p = GetTriggerPlayer()
local thistype this = thistype(GetPlayerId(p))
local group g = CreateGroup()
local unit u
local real mx = GetMouseX(p)
local real my = GetMouseY(p)
local real dx
local real dy
local unit selectedUnit = null
local real distanceSqr = 999999.9
local real dSqr
call GroupEnumUnitsInRange(g, mx, my, 80.0, null)
loop
set u = FirstOfGroup(g)
exitwhen u == null
set dx = GetUnitX(u) - mx
set dy = GetUnitY(u) - my
set dSqr = dx * dx + dy * dy
if .u != u and UnitAlive(u) and dSqr < distanceSqr then
set distanceSqr = dSqr
set selectedUnit = u
endif
call GroupRemoveUnit(g,u)
endloop
call DestroyGroup(g)
set .target = selectedUnit
if p == GetLocalPlayer() then
if target == null then
call clearUiTarget(this)
//call BJDebugMsg("no target")
else
//call BJDebugMsg("Target: " + GetUnitName(target))
call setUiTarget(.target)
endif
endif
set selectedUnit = null
set g = null
set p = null
endmethod
private static method onClick takes nothing returns nothing
if UserMouse[GetTriggerPlayer()].isMouseButtonClicked(MOUSE_BUTTON_TYPE_LEFT) then
call selectTargetNearMouse()
endif
endmethod
public method updateAbilityDetails takes nothing returns nothing
call attack.updateAbilityDetails()
call attackAlt.updateAbilityDetails()
call util1.updateAbilityDetails()
call util2.updateAbilityDetails()
call dash.updateAbilityDetails()
call interact.updateAbilityDetails()
call map.updateAbilityDetails()
endmethod
private static method onInit takes nothing returns nothing
local player p
local integer i = 0
local thistype this
set mc_main_down = CreateTrigger()
set mc_main_up = CreateTrigger()
call TriggerAddCondition( mc_main_down, Condition(function thistype.keyPressed))
call TriggerAddCondition( mc_main_up, Condition(function thistype.keyReleased))
call OnMouseEvent(function thistype.onClick, EVENT_MOUSE_DOWN)
loop
exitwhen i > 4
set p = Player(i)
set this = thistype(i)
set .isDoingAction = false
set .isHolding = false
if GetPlayerController(p) == MAP_CONTROL_USER and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING then
//BlzTriggerRegisterPlayerKeyEvent takes trigger whichTrigger, player whichPlayer, oskeytype key, integer metaKey, boolean keyDown returns event
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_ATTACK, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_ATTACK, 0, false)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_ATTACK_ALT, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_ATTACK_ALT, 0, false)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_UTIL_1, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_UTIL_1, 0, false)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_UTIL_2, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_UTIL_2, 0, false)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_DASH, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_DASH, 0, false)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_INTERACT, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_INTERACT, 0, false)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_down, p, MC_KEY_MAP, 0, true)
call BlzTriggerRegisterPlayerKeyEvent(mc_main_up, p, MC_KEY_MAP, 0, false)
endif
set i = i + 1
endloop
set p = null
endmethod
public static method setAttack takes player p, HeroActionBase attack returns nothing
local thistype this = thistype(GetPlayerId(p))
set .attack = attack
call updateAbilityDetails()
endmethod
public static method setAttackAlt takes player p, HeroActionBase attackAlt returns nothing
local thistype this = thistype(GetPlayerId(p))
set .attackAlt = attackAlt
call updateAbilityDetails()
endmethod
public static method setUtil1 takes player p, HeroActionBase util1 returns nothing
local thistype this = thistype(GetPlayerId(p))
set .util1 = util1
call updateAbilityDetails()
endmethod
public static method setUtil2 takes player p, HeroActionBase util2 returns nothing
local thistype this = thistype(GetPlayerId(p))
set .util2 = util2
call updateAbilityDetails()
endmethod
public static method setDash takes player p, HeroActionBase dash returns nothing
local thistype this = thistype(GetPlayerId(p))
set .dash = dash
call updateAbilityDetails()
endmethod
public static method setMap takes player p, HeroActionBase map returns nothing
local thistype this = thistype(GetPlayerId(p))
set .map = map
endmethod
public static method setInteract takes player p, HeroActionBase interact returns nothing
local thistype this = thistype(GetPlayerId(p))
set .interact = interact
endmethod
public static method setHero takes player p, unit hero returns nothing
local thistype this = thistype(GetPlayerId(p))
set .u = hero
endmethod
public static method setTarget takes player p, unit target returns nothing
local thistype this = thistype(GetPlayerId(p))
set .target = target
endmethod
public method getAttack takes nothing returns HeroActionBase
return .attack
endmethod
public method getAttackAlt takes nothing returns HeroActionBase
return .attackAlt
endmethod
public method getUtil1 takes nothing returns HeroActionBase
return .util1
endmethod
public method getUtil2 takes nothing returns HeroActionBase
return .util2
endmethod
public method getDash takes nothing returns HeroActionBase
return .dash
endmethod
public method getMap takes nothing returns HeroActionBase
return .map
endmethod
public method getInteract takes nothing returns HeroActionBase
return .interact
endmethod
public method getTargetUnit takes nothing returns unit
return .target
endmethod
public method getHero takes nothing returns unit
return .u
endmethod
public method setTargetUnit takes unit newTarget returns nothing
set .target = newTarget
endmethod
//call MetroidvaniaController.disableControl()
public static method disableControl takes nothing returns nothing
call ClearSelection()
endmethod
public static method resumeControl takes nothing returns nothing
call SelectUnit(thistype(GetPlayerId(GetLocalPlayer())).u, true)
endmethod
public static method getTargetX takes player p returns real
//call BJDebugMsg("getTargetX: Playerid=" + I2S(GetPlayerId(p)))
return thistype(GetPlayerId(p)).currentActionTargetX
endmethod
public static method getTargetY takes player p returns real
return thistype(GetPlayerId(p)).currentActionTargetY
endmethod
public static method getMetroidvaniaController takes player p returns MetroidvaniaController
return thistype(GetPlayerId(p))
endmethod
endstruct
function controllerCallback takes unit u, HeroActionBase action returns nothing
call MetroidvaniaController.controllerCallback(u, action)
endfunction
function GetHero takes player p returns unit
return MetroidvaniaController.getMetroidvaniaController(p).getHero()
endfunction
endlibrary
Heroes can have "targets" selected. When firing an attack, you fire towards the target (if you have one) and towards mouse-location otherwise.
The idea is that different heroes can have different abilities on different keys.
In the map (linked), the
MouseUtils
can be found in ImportedSystems/MouseUtils
"trigger", then there are CustomSystems/MouseUtilsLockCamExtension
and CustomSystems/HeroController
for the Lock-cam solution described, and the Controller that requests the mouse location on bealf of abilities.Abilities can be found in
Hero/HeroAbilities
.I have not been able to replicate this using 2 instances on the same machine, because when alt+tabing to a WC3 instance, you instantly fire a mouse-event when entering the game, so it always "works" when playing LAN on the same computer, because the WC3 instance you're playing is the one that had its mouse-event fire last...
Sorry for the wall of text, but it isn't all that trivial to describe...