User:ArthurAutomaton/sandbox: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
Line 98: Line 98:
== Passing an argument to a thinker ==
== Passing an argument to a thinker ==


'''Problem:''' You want to start a thinker at a certain time during the game, and you want this thinker to have access to some information that's only available at this time.
=== Problem ===
You want to start a thinker at a certain time during the game, and you want this thinker to have access to some information that's only available at this time.


'''Solution:''' Take advantage of the fact that functions in Lua can access variables of their enclosing functions:
=== Solution ===
Take advantage of the fact that functions in Lua can access variables of their enclosing functions:


<source lang="lua">
<source lang="lua">

Revision as of 01:21, 31 August 2014

Sandbox

Some notes to myself about modding DotA 2.

Creating a unit that's controllable by a specific player

This code creates a mud golem at (0, 0, 0) on the Radiant team and makes it controllable by player 0:

 local unit_team = DOTA_TEAM_GOODGUYS
 local unit_name = "npc_dota_neutral_mud_golem"
 local player = PlayerResource:GetPlayer(0)
 local point = Vector(0, 0, 0)
 
 local unit = CreateUnitByName(unit_name, point, true, player, player:GetAssignedHero(), unit_team)
 unit:SetControllableByPlayer(player:GetPlayerID(), true)


Relevant links:

Triggers and shared mutable state

Problem

You have two triggers, trigger_get and trigger_set. When trigger_set is triggered, you want to save some information in a certain global variable. When trigger_get is triggered, you should be able to access that information.

An approach that doesn't work

Warning.pngWarning:Here is a common approach that DOES NOT WORK

1. Create scripts/vscripts/trigger.lua with the following content:

 FOO = 0 -- we want to save information in this global variable
 
 function SetSharedVar ()
    FOO = 1
    print("SetSharedVar called; FOO = " .. FOO)
 end
 
 function GetSharedVar ()
    print("GetSharedVar called; FOO = " .. FOO)
 end

2. In Hammer, edit the properties of trigger_get as follows:

  • Set Entity Scripts = trigger.lua.
  • Add an output like this:
My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnStartTouch trigger_get CallScriptFunction GetSharedVar 0.00 No

3. In Hammer, edit the properties of trigger_set as follows:

  • Set Entity Scripts = trigger.lua.
  • Add an output like this:
My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnStartTouch trigger_set CallScriptFunction SetSharedVar 0.00 No

The expected outcome: When a unit steps on trigger_set and then afterwards steps on trigger_get, we expect to see the following output in the console:

SetSharedVar called; FOO = 1
GetSharedVar called; FOO = 1

What really happens: What we actually see in the console is this:

SetSharedVar called; FOO = 1
GetSharedVar called; FOO = 0

This shows that the assignment FOO = 1 made by SetSharedVar is NOT visible to trigger_get.

My guess about why this happens: When you "bind" some Lua code to an entity by setting its Entity Scripts property, that code is "private" to the entity: The entity initially has ITS OWN "blank" Lua environment (with only the library functions and the game API visible), which no other entity can access. When the map is loaded, the game engine populates this environment by running the Lua file specified by the Entity Scripts property. In our case, we set Entity Scripts = trigger.lua on both triggers, but they both get THEIR OWN COPIES of the global variables in trigger.lua. So the assignment FOO = 1 made by trigger_set is not visible to trigger_get since it just changes trigger_set's copy of FOO.

An approach that works

Follow steps 1-3 above but replace Step 2 with this:

2'. In Hammer, edit the properties of trigger_get as follows:

  • DO NOT SET THE Entity Scripts PROPERTY
  • Add an output like this (note that we now use trigger_set as the target):
My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnStartTouch trigger_set CallScriptFunction GetSharedVar 0.00 No

With this solution we get the expected output in the console. Why? If the guess above is correct, it's because we're now using trigger_set as the target, hence GetSharedVar now accesses trigger_set's copy of FOO.

Holdout stuff

  • For the bosses they use the vscripts key (in npc_units_custom.txt) to set the AI file that should be used.
    • In the AI files the global variable thisEntity (which is not initialized anywhere in the files) seems to refer to the unit itself.
    • Also it seems that if there's a function called Spawn in the file, then it will be called (with a certain table as argument) when the unit is spawned.
    • How does Ogre Magi get its AI file hooked up?

Passing an argument to a thinker

Problem

You want to start a thinker at a certain time during the game, and you want this thinker to have access to some information that's only available at this time.

Solution

Take advantage of the fact that functions in Lua can access variables of their enclosing functions:

 -- addon_game_mode.lua

 function Activate ()
    ListenToGameEvent("entity_killed", ExactDelayedRevenge, nil)
 end

 -- Three seconds after an entity has been killed its attacker will be killed
 function ExactDelayedRevenge (keys)
    local attacker = EntIndexToHScript(keys.entindex_attacker)
    GameRules:GetGameModeEntity():SetThink(function ()
          -- We can access attacker since it's a variable of our enclosing function
          if attacker and attacker:IsAlive() then
             attacker:ForceKill(false)
          end
    end, 3)
 end

Almost interactive development

Tip.pngTip:Typing "script_reload_code test" in the console will cause the Lua code in scripts/vscripts/test.lua to be executed.

Changing properties of a hero on their first spawn

Problem

You need to initialize a hero through Lua when it spawns for the first time (which is normally when it gets selected by a player through the hero selection screen).

Solution

Register a listener on the "npc_spawned" event. The listener should check whether the spawned NPC is a real hero, in which case it should initialize the NPC and mark it as initialized (so that it doesn't get re-initialized the next time it spawns):

 -- addon_game_mode.lua

 local INIT_MARK = DoUniqueString("INIT") -- For marking a hero as initialized

 function Activate ()
    ListenToGameEvent("npc_spawned", OnNpcSpawned, nil)
 end

 function OnNpcSpawned (keys)
    local npc = EntIndexToHScript(keys.entindex)
    if npc and npc:IsRealHero() and not npc[INIT_MARK] then
       -- Initialize hero here, e.g.:
       npc:GetAbilityByIndex(0):SetLevel(3)
       npc[INIT_MARK] = true
    end
 end

Discussion

We can improve the above solution in at least two ways:

  1. The variable INIT_MARK is currently visible to the whole file. When there's a lot of code in the file, this can make it hard to figure out where the variable is used.
  2. We can make the OnNpcSpawned function easier to extend. Right now, if we want to use the function to initialize non-heroes, we have to change both the test and the body of the if statement.

We can solve both problems by introducing a "factory" function that takes an initialization function as argument:

 -- addon_game_mode.lua

 function Activate ()
    ListenToGameEvent("npc_spawned", MakeNpcInitializer(InitializeHero), nil)
 end

 -- Factory function for making listeners
 function MakeNpcInitializer (initFn)
    local initMark = DoUniqueString("INIT") -- private to this factory function
    return function (keys)
       local npc = EntIndexToHScript(keys.entindex)
       if npc and not npc[initMark] then
          initFn(npc)
          npc[initMark] = true
       end
    end
 end

 function InitializeHero (npc)
    if npc:IsRealHero() then
       -- Initialize hero here, e.g.:
       npc:GetAbilityByIndex(0):SetLevel(3)
    end
 end

Cancelling a thinker

Problem

You need to cancel a thinker that you have started.

Solution

Let the thinker return nil; this will cause it to stop:

 -- some_trigger.lua

 local IS_CANCELLED = false
 local THINK_INTERVAL = 1

 function OnStartTouch ()
    GameRules:GetGameModeEntity():SetThink(function ()
          if IS_CANCELLED then
             return nil -- Returning nil stops the thinker
          else
             print("not cancelled yet; proceeding")
             return THINK_INTERVAL
          end
    end)
 end

 function OnEndTouch ()
    IS_CANCELLED = true
 end

Discussion

The above example is quite crude. Provided that some_trigger.lua is set as the entity script of a trigger with the appropriate outputs, it will work like this: Every time a unit steps onto the trigger, a thinker will be started that prints every second. When a unit steps off the trigger, all these thinkers will stop. We can refine this as follows:

 -- some_trigger.lua

 local CANCEL_MARK = DoUniqueString("CANCEL")
 local THINK_INTERVAL = 1

 function OnStartTouch (keys)
    local activator = keys.activator
    activator[CANCEL_MARK] = nil
    activator:SetThink(function ()
          if activator and not activator[CANCEL_MARK] then
             -- Not cancelled yet; do some stuff here
             print(activator:GetName())
             return THINK_INTERVAL
          end
    end)
 end

 function OnEndTouch (keys)
    keys.activator[CANCEL_MARK] = true
 end

This code has the following effect: Every time a unit steps onto the trigger, a thinker starts that prints the unit's name every second. When a unit steps off the trigger, his thinker is cancelled.