User:ArthurAutomaton/sandbox: Difference between revisions
m (→Event details) |
|||
Line 249: | Line 249: | ||
== Listening to game events == | == Listening to game events == | ||
This is a tutorial for beginners. | This is a tutorial for beginners. If you see something that can be improved, please improve it! | ||
=== Introduction === | === Introduction === | ||
Line 312: | Line 312: | ||
; context | ; context | ||
: Here you can use either <code>nil</code> or an object (for example, a table or a function). If you use an object here, then it will be passed as the first argument to your function when the event happens. | : Here you can use either <code>nil</code> or an object (for example, a table or a function). If you use an object here, then it will be passed as the first argument to your function when the event happens. | ||
=== Event details === | === Event details === | ||
Line 371: | Line 367: | ||
ListenToGameEvent(<span style="color:red">"some_event_name"</span>, Dynamic_Wrap(MyClass, "MyFunction"), <span style="background:blue; color:white;">?</span>) | ListenToGameEvent(<span style="color:red">"some_event_name"</span>, Dynamic_Wrap(MyClass, "MyFunction"), <span style="background:blue; color:white;">?</span>) | ||
Replace <code><span style="background:blue; color:white;">?</span></code> with the object that you want to use as <code>self</code> in your function. | Replace <code><span style="background:blue; color:white;">?</span></code> with the object that you want to use as <code>self</code> in your function. Here is an example: | ||
==== Example 3 ==== | ==== Example 3 ==== |
Revision as of 06:14, 6 September 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:
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

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:
3. In Hammer, edit the properties of trigger_set
as follows:
- Set Entity Scripts = trigger.lua.
- Add an output like this:
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):
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 (innpc_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?
- In the AI files the global variable
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

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:
- 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. - 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.
Listening to game events
This is a tutorial for beginners. If you see something that can be improved, please improve it!
Introduction
This tutorial is about the API function ListenToGameEvent
. If you call ListenToGameEvent
with an event name and a function, the game will call that function every time the event happens.
Example 1
Every time a player levels up, we show a message to all players:
-- scripts/vscripts/addon_game_mode.lua
function LevelUpMessage (eventInfo)
Say(nil, "Someone just leveled up!", false)
end
function Activate ()
ListenToGameEvent("dota_player_gained_level", LevelUpMessage, nil)
end
We have two functions here:
LevelUpMessage
: Here we use the API functionSay
to show a message to all players.Activate
: The game calls this function when it loads the addon. We want the game to callLevelUpMessage
every time a player levels up. To do this we call the API functionListenToGameEvent
with"dota_player_gained_level"
andLevelUpMessage
as arguments.
Example 2
When a player reaches level 6, we show a message to all players:
-- scripts/vscripts/addon_game_mode.lua
function Level6Message (eventInfo)
if eventInfo.level == 6 then
Say(nil, "Someone just reached level 6", false)
end
end
function Activate ()
ListenToGameEvent("dota_player_gained_level", Level6Message, nil)
end
Note: In Level6Message
we use the argument, eventInfo
, to get the level that the player has reached.
The function ListenToGameEvent
Here is the signature of ListenToGameEvent
:
int ListenToGameEvent(string eventName, handle functionToCall, handle context)
Parameters
- eventName
- The name of the event that you want to listen to. See the page Built-In Engine Events for a complete list.
- functionToCall
- The function that you want the game to call when the event happens.
- context
- Here you can use either
nil
or an object (for example, a table or a function). If you use an object here, then it will be passed as the first argument to your function when the event happens.
Event details
By calling ListenToGameEvent
with an event name and a function, you tell the game to call that function every time the event happens. When the game calls the function, it passes a table as an argument to it. This table contains details of the event. On the page Built-In Engine Events you can see which keys the table has. For example, on this page you can see that the table for "dota_player_gained_level"
has a key called level
(cf. Example 2 above).

pairs
function to see which keys a table has:
for key,val in pairs(tbl) do
print(key, val)
end
tbl
with the table you want to inspect.)Your listener function
If your function is defined like this:
function MyFunction (arg)
-- some code here
end
then call ListenToGameEvent
like this:
ListenToGameEvent("some_event_name", MyFunction, nil)
But what if your function is defined like this?
function MyClass:MyFunction (arg)
-- some code here
end
Remember: This is a short way of writing
function MyClass.MyFunction (self, arg)
-- some code here
end
So your function actually takes two arguments. In this situation, call ListenToGameEvent
like this:
ListenToGameEvent("some_event_name", MyClass.MyFunction, ?)
OR like this:
ListenToGameEvent("some_event_name", Dynamic_Wrap(MyClass, "MyFunction"), ?)
Replace ?
with the object that you want to use as self
in your function. Here is an example:
Example 3
-- scripts/vscripts/addon_game_mode.lua
if MyClass == nil then
MyClass = class({})
end
function MyClass:InitGameMode ()
self.numSpawned = 0
ListenToGameEvent("npc_spawned", MyClass.MyFunction, self)
end
function MyClass:MyFunction ()
self.numSpawned = self.numSpawned + 1
print("Number of NPCs spawned so far: " .. self.numSpawned)
end
function Activate ()
GameRules.MyAddon = MyClass()
GameRules.MyAddon:InitGameMode()
end