evennia/docs/sphinx/source/wiki/TickerScripts.rst
2013-05-15 23:49:38 +02:00

232 lines
9.5 KiB
ReStructuredText

Ticker Scripts ("Heartbeats")
=============================
A common way to implement a dynamic MUD is by using "tickers", also
known as "heartbeats". A ticker is a timer that fires ("ticks" at a
given interval. The tick triggers updates in various game systems.
Tickers are very common or even unavoidable in other mud code bases,
where many systems are hard coded to rely on the concept of the global
'tick'. Evennia has no such notion - the use of tickers (or not) is very
much up to your game and which requirements you have.
`Scripts <Scripts.html>`_ are powerful enough to act as any type of
counter you want and the "ticker recipe" is just one convenient (and
occationally effective) way of cranking the wheels.
When \_not\_ to use tickers
---------------------------
First, let's consider when *not* to use tickers. Even if you are used to
habitually relying on tickers for everything in other code bases, stop
and think about what you really need them for. Notably you should
*never* try implement a ticker to *catch changes*. Think about it - you
might have to run the ticker every second to react to the change fast
enough. Most likely nothing will have changed most of the time. So you
are doing pointless calls (since skipping the call gives the same result
as doing it). Making sure nothing's changed might even be
computationally expensive depending on the complexity of your system.
Not to mention that you might need to run the check on every object in
the database. Every second. Just to maintain status quo.
Rather than checking over and over on the off-chance that something
changed, consider a more proactive approach. Can you maybe implement
your rarely changing system to *itself* report its change *whenever* it
happens? It's almost always much cheaper/efficient if you can do things
"on demand". Evennia itself uses hook methods for this very reason. The
only real "ticker"-like thing in the default set is the one that saves
the uptime (which of course *always* changes every call).
So, in summary, if you consider a ticker script that will fire very
often but which you expect to do nothing 99% of the time, ponder if you
can handle things some other way. A self-reporting solution is usually
cheaper also for fast-updating properties. The main reason you do need a
ticker is rather when the timing itself is important.
Ticker example - night/day system
=================================
Let's take the example of a night/day system. The way we want to use
this is to have all outdoor rooms echo time-related messages to the
room. Things like "The sun sets", "The church bells strike midnight" and
so on.
One could imagine every `Room <Objects.html>`_ in the game having a
script running on themselves that fire regularly. It's however much
better (easier to handle and using a lot less computing resources) to
use a single, global, ticker script. You create a "global" Script the
way you would any Script except you don't assign it to any particular
object.
To let objects use this global ticker, we will utilize a *subscription
model*. In short this means that our Script holds an internal list of
"subscribing" rooms. Whenever the Script fires it loops through this
list and calls a given method on the subscribed object.
::
from ev import Script
class TimeTicker(Script):
"""
This implements a subscription model
"""
def at_script_creation(self):
"Called when script is created"
self.key = "daynight_ticker"
self.interval = 60 * 60 * 2 # every two hours
self.persistent = True
# storage of subscriptions
self.db.subscriptions = []
def subscribe(self, obj):
"add object to subscription"
if obj not in self.db.subscriptions:
self.db.subscriptions.append(obj)
def unsubscribe(self, obj):
"remove object from subscription"
try:
del_ind = self.db.subscriptions.index(obj)
del self.db.subscriptions[del_ind]
except ValueError:
pass
def list_subscriptions(self):
"echo all subscriptions"
return self.db.subscriptions
def at_repeat(self):
"called every self.interval seconds"
for obj in self.db.subscriptions:
obj.echo_daynight()
This depends on your subscribing weather rooms defining the
``echo_daynight()`` method (presumably outputting some sort of message).
It's worth noting that the simple recipe above can be used for all sorts
of tickers. Rooms are maybe not likely to unsubscribe very often, but
consider a mob that "deactivates" when Characters are not around for
example.
This particular TimeTicker-example could be further optimized. All
subscribed rooms are after all likely to echo the same time related
text. So this text can be pre-set already at the Script level and echoed
to each room directly. This way the subscribed objects won't need a
custom ``echo_daynight()`` method at all.
Here's the more efficient example (only showing the new stuff).
::
...
ECHOES = ["The sun rises in the east.",
"It's mid-morning",
"It's mid-day", ...]
class TimerTicker(Script):
...
def at_script_creation(self):
...
self.db.timeindex = 0
...
def at_repeat(self):
"called every self.repeat seconds"
echo = ECHOES[self.db.timeindex]
# msg_contents() is a standard method, so this
# ticker works with any object.
for obj in self.db.subscriptions:
obj.msg_contents(echo)
# resetting/stepping the counter
if self.db.timeindex == len(ECHOES) - 1:
self.db.timeindex = 0
else:
self.db.timeindex += 1
Note that this ticker is unconnected to Evennia's default global in-game
time script, and will thus be out of sync with that. A more advanced
example would entail this script checking the current game time (in
``at_script_creation()`` or in ``at_start()``) so it can pick a matching
starting point in its cycle.
Testing the night/day ticker
----------------------------
Tickers are really intended to be created and handled from your custom
commands or in other coded systems. An "outdoor" room typeclass would
probably subscribe to the ticker itself from its
``at_object_creation()`` hook. Same would be true for mobs and other
objects that could respond to outside stimuli (such as the presence of a
player) in order to subscribe/unsubscribe.
There is no way to create a global script using non-superuser commands,
and even if you could use ``@script`` to put it on an object just to
test things out, you also need a way to subscribe objects to it.
With ``@py`` this would be something like this:
::
@py ev.create_script(TimeTicker) # if persistent=True, this only needs to be done once
@py ev.search_script("daynight_ticker").subscribe(self.location)
If you think you will use these kind of ticker scripts a lot, you might
want to create your own command for adding/removing subscriptions to
them. Here is a complete example:
::
import ev
class CmdTicker(ev.default_cmds.MuxCommand):
"""
adds/remove an object to a given ticker
Usage:
@ticker[/switches] tickerkey [= object]
Switches:
add (default) - subscribe object to ticker
del - unsubscribe object from ticker
This adds an object to a named ticker Script,
if such a script exists. Such a script must have
subsribe/unsubscripe functionality. If no object is
supplied, a list of subscribed objects for this ticker
will be returned instead.
"""
key = "@ticker"
locks = "cmd:all()"
help_category = "Building"
def func(self):
if not self.args:
self.caller.msg("Usage: @ticker[/switches] tickerkey [= object]")
return
tickerkey = self.lhs
# find script
script = ev.search_scripts(tickerkey)
if not script:
self.caller.msg("Ticker %s could not be found." % tickerkey)
return
# all ev.search_* methods always return lists
script = script[0]
# check so the API is correct
if not (hasattr(script, "subscribe")
and hasattr(script, "unsubscribe")
and hasattr(script, "list_subscriptions"):
self.caller.msg("%s can not be subscribed to." % tickerkey)
return
if not self.rhs:
# no '=' found, just show the subs
subs = [o.key for o in script.list_subscripionts()]
self.caller.msg(", ".join(subs))
return
# get the object to add
obj = self.caller.search(self.rhs)
if not obj:
# caller.search handles error messages
return
elif 'del' in self.switches:
# remove a sub
script.unsubscribe(obj)
self.caller.msg("Unsubscribed %s from %s." % (obj.key, tickerkey)
else:
# default - add subscription
script.subscribe(obj)
self.caller.msg("Subscribed %s to ticker %s." % (obj.key, tickerkey))
This looks longer than it is, most of the length comes from comments and
the doc string.