mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
232 lines
9.5 KiB
ReStructuredText
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.
|