Character Simulation with ScriptX

The ScriptX system from Kaleida Labs is a platform-independent, object-oriented development environment for creating multimedia apps. Assaf implements a ScriptX-based multimedia application which was created in a way that facilitates reuse of its design and code in other simulations.


November 01, 1994
URL:http://www.drdobbs.com/tools/character-simulation-with-scriptx/184409348

Figure 1


Copyright © 1994, Dr. Dobb's Journal

Figure 2


Copyright © 1994, Dr. Dobb's Journal

Figure 3


Copyright © 1994, Dr. Dobb's Journal

Figure 4


Copyright © 1994, Dr. Dobb's Journal

Figure 1


Copyright © 1994, Dr. Dobb's Journal

Figure 2


Copyright © 1994, Dr. Dobb's Journal

Figure 3


Copyright © 1994, Dr. Dobb's Journal

Figure 4


Copyright © 1994, Dr. Dobb's Journal

NOV94: Character Simulation with ScriptX

Character Simulation with ScriptX

A general-purpose framework for dynamic behavior

Assaf Reznik

Assaf is a senior multimedia designer at Kaleida Labs, where he developed Playfarm with Lisa Lopuck, Mike Powers, and other engineers. He can be contacted at [email protected].


Filmore T. Goose is an obsessively neat bird, the Felix Unger of the animal world, who continuously picks up after fellow animals. Irma La Sheep is a good-hearted quadruped, always ready to knit a sweater as a gift for any animal that happens to pass by. These are two of the characters in Playfarm, a prototype multimedia title for children implemented in ScriptX from Kaleida Labs. ScriptX is a platform-independent, object-oriented development environment for creating multimedia applications (see the accompanying text box, "Introducing ScriptX").

Playfarm consists of a virtual farm environment populated with some interesting and truly autonomous characters; see Figure 1. Every character in the Playfarm simulation runs in its own thread and can interact with the other characters in a rich, complex, and unpredictable manner. Playfarm's open-ended structure allows you to dynamically add new characters into the environment while it is running. This article describes the architecture and implementation of Playfarm, which was designed in a general-purpose way so that its design and code can be reused in creating other simulations.

Design Goals

Many current multimedia titles are structured as a series of film clips stitched together in a static way that sometimes provides a convincing illusion of dynamic behavior--even though every sequence has been planned, scripted, and burned into code before execution time. However, we wanted our characters to interact in a more dynamic fashion, rather than in the usual, predetermined way. At each clock tick, characters sense, process, and react to their surroundings. Their reaction depends both on the state of the environment at a given time, and on their own internal state. This free-form response is implemented by a character's "activity engine," discussed later.

A related design goal was to create characters with real personalities which reveal themselves over time. Behavior must be continuous rather than "action bites." Characters respond to the environment and to the user. When Playfarm props and characters are added, removed, or otherwise changed, characters must continue to interact with their surroundings in a manner consistent with their personalities.

Another goal was to make the environment extensible, creating an experience that users could modify and control, if so inclined. An additional goal was to create a general-purpose, content-independent model for character-based simulation. By creating an abstract model that is decoupled from the presentation layer, this model can be used in other contexts. Our approach is guided by the well-known Model-View-Controller (MVC) paradigm, first used in Smalltalk-80 and now incorporated into modern class libraries, including ScriptX.

Playfarm Architecture

The Playfarm architecture consists of three components: simulation environment, character and prop, and user representation. The user-representation component is the simplest, consisting of a geometric space equal in size to the screen display, and an intelligent cursor object.

The simulation-environment component is a general-purpose framework for a character-based simulation. It is divided into the presentation space and the model space; see Figure 2. The presentation space is roughly analogous to the View portion of the Smalltalk MVC. It contains all visible objects (except for the cursor, which belongs to the user-representation component), as well as objects that manipulate the visual aspects of the environment--such as the Panner object, which is responsible for panning or moving around the model space. Only the section of the farm that is visible at a given time is part of the presentation space. By contrast, the model space covers the whole span of the virtual farm.

The character-and-prop component is, as you might expect, the set of classes used to instantiate characters and props. Props are inanimate objects in the space, while characters have animated behavior. Figure 3 illustrates the structure of a Playfarm character, which consists of an animation object and a character shell. The animation object contains all the raw animation sequences for a character. Each of these sequences is named so that the model object can refer to it. Whenever the model makes an animation request, the name of the requested animation sequence is added to the animation queue and then played in order.

The character shell contains the character's repertoire of activities and the logic describing the relationships between this set of activities and factors such as time and mood (which in itself has a relationship to the environment and to other characters). These relationships are modeled in the character's "activity engine." Excerpts from the Playfarm source code are shown in Listing One . The complete code (about 2000 lines of ScriptX) is available electronically; see "Availability," page 3.

The Model Space

Playfarm maps the virtual farm onto a simulation-modeling space. This is a geometric mapping. As shown in Figure 4, it consists of a web of triangles whose nodes form a series of equilateral triangles. From each node, a character can potentially move in six directions: right, down/right, down/left, left, up/left, and up/right.

This kind of grid has several advantages over other designs. Unlike a traditional rectangular grid (with four directions of movement), it is not immediately obvious to the user that characters can only move in six possible directions. Also, unlike a three-dimensional model, the flat structure of the Playfarm grid significantly reduces the complexity of character-animation sequences:There are only two basic character orientations (left- and right-facing), obviating the need for four views of a character (back, front, left, right).

Although the farm is presented on a vertical (visually upright) screen from a frontal view point, the mapping is of an aerial view. Each row, then, represents a different z-plane in the virtual farm. This is translated to the presentation layer by setting the z instance variables of corresponding presenters in each row to reflect the depth. Thus we can achieve a two-and-a-half-dimensional perspective.

The class Grid consists of nodes which are instances of class Cell (both are available electronically). It contains the behavior objects of Playfarm--that is, prop shells and character shells. The Cell class, among other things, implements a basic set of services for moving from one cell to any of its six neighbors, for adding and deleting objects from a cell, and for accessing a cell's contents and that of its neighboring cells.

The Character Component

In designing a Playfarm character, the first step is creating some animation sequences. You can do this with tools such as Macromedia Director. Cells created with Director must be converted to ScriptX's internal object representation, Bento, using ScriptX's import facility. Bento is an object-storage format designed at Apple and used in other software systems such as OpenDoc and the Taligent Frameworks. There is a Bento file associated with each character.

Next, these individual frames must be combined into a meaningful, named sequence. The Playfarm function makeFrameSequence takes a Bento file, a direction in which a character moves while performing the animation, the number of nodes to move (0 or 1 in Playfarm), the bitmaps associated with that sequence, and an optional sound argument. This function returns a FrameSequence object that, when properly attached to a movement controller, animates through all cells of the animation and smoothly moves the character in the specified direction at a velocity synchronized with the flip animation. All sequences in Playfarm are created such that they either start and end at the same location or move the animated object exactly one cell. For more details on the Animation class, see Listing One.

In the class SheepPresenter For Irma, sequences are defined for walking, facing, turning (all these in the six possible directions), as well as for knitting, eating, complaining, and so on. Then the animation object is told what the transition sequences are--in this case, turnLeft and turnRight--and which sequences a character can go to from each transition. The model-character object is unaware of the names of transition sequences; they are abstracted from the behavior layer. Thus, when the behavior component of the character decides to walk left when the character is walking right, the animation object contains the logic that a transition sequence needs to be played before the character can walk left. This completes the presentation aspect of the character.

Character Behavior

The class Shell is listed in Listing One, along with its derivative CharacterShell. Together, these two encapsulate the behavior aspect of a character: attributes, current state, direction, relation to the model grid, and relation to a target object with which it might be choreographed. The Shell class and its subclasses provide services for keeping model shells and shell presenters in sync with each other. The Shell class also provides services for adding new shells, with their presenters, to the scene. To create the behavior shell for Irma, we must inherit from CharacterShell. This wires the sheep object to the model space and to its presenter.

The meat, if you will, of the sheep shell is its logical actions. The sheep presenter's animation sequences are combined into actions that make sense by defining character actions in the init method for the Sheep object. These actions rely on action classes, which provide the general building blocks for creating meaningful activities for characters. Each action instance is one atomic thing a character does, such as "walk left" or "knit a sweater." Combining these atomic actions into sequences and branches results in interesting behaviors and coherent interactions between objects in the system. Actions are the wires that connect a character's activity engine with the model space and the animation class.

Actions are combined together by two mechanisms: nested actions and action sequences. In the simplest case, an action simply corresponds to one animation sequence. The animation object (sheep presenter) is referred to by the targetAction instance variable of the Action object. But targetAction can also refer to a nested action or action sequence. In this case, when the action is invoked, it will invoke yet another action after doing some processing of its own. Sequencing several actions together in a meaningful context creates an activity.

Irma possesses a wander-around activity, a knit-sweater activity, and a complain activity. complainActivity, for example, is a sequence of three wander actions and a random action, which is nested. Sixty percent of time that complainActivity is invoked, it calls complainAction (the rest of the time it returns with no effect).

The Character Engine

The last step in creating a character is to install character activities into the activity engine. The approach described here was influenced by Patie Maes' work at MIT Media Lab. The character engine is the main driver of the character component. Character-level decisions define the overall expression of characters. For example, a character engine may make a character decide to sleep when it is tired, clean when it is anxious, and knit sweaters when it encounters a friend. The character engine understands schedules (biological clocks), moods, and the character's sense of the environment. It weighs all these different values and makes changes in its activities as needed.

The character engine models a character's traits and then influences the choice of activities that the character undertakes over time. Each activity is associated with a weight. This weight changes dynamically through the experience. Weights are influenced by factors such as the schedule, motivations, and reactions to the environment. For example, consider a dog character whose activity repertoire consists of eat, sleep, run around, and chase a cat. Each of these activities is assigned an initial weight. Then, if the dog is old and lazy, we create a schedule that favors sleeping and eating activities most hours of the day (or what ever time unit is abstracted by the schedule). The tiredness motivation goes up each time the character ends up running or chasing a cat.

Schedules link the weights of certain behaviors to the passage of time. One example is a 24-hour clock that influences a different behavior for each hour of the day. In our dog example, the schedule may read: eat at 8 a.m., run around at 9, chase cat at 10, eat at 11, and so on. Possible patterns can be seasonal, circadian, or simple rhythms. At each cycle of the activity engine, appropriate activity for that time is selected. Then, that weight associated with it is added to the active weight for that activity.

Strengtheners can strengthen an activity or a schedule. For example, if the dog chases a cat whenever one is in the vicinity, we can use a strengthener on the schedule to check if the current activity is the scheduled one. If it isn't, the strengthener will magnify the scheduled activity even more, thereby enforcing the schedule.

Motivations are externally attached personality factors of the character. This is how a character can pick up the feeling of a locale or experience. Motivations are usually modeled as range values that can be lowered or raised--for example, our dog has a loner motivation, which is triggered by a sensor of too_much_commotion around our dog. Whenever the sensor reads that the dog is not alone (that is, other characters are around), the loner motivation is pushed higher. In this way the dog might choose behavior based on its desire to be alone.

Reactions are sets of activities triggered directly by events (signaled by sensors). This allows characters to be unconditionally responsive to events no matter what their mood is at the time; for example, when hit by lightning.

Importing a Character

One of Playfarm's most attractive features is that it allows end users to import characters and props at run time. After a user spends some time with Playfarm, it can evolve into the user's own creation.

The process of developing and importing an independent character is very simple: You must provide the system with the presenter definition, the character-shell definition, and the object store containing the media. These steps have all been described here. The only remaining steps are to put the new character in its initial location, add it to the scene, and start its activity engine.

Introducing ScriptX

Ray Valdés

ScriptX is a multimedia-oriented development environment created over the last two years by Kaleida Labs. Unlike packages such as Macromedia Director, ScriptX is not an authoring tool for creating multimedia titles, although it does come with a built-in authoring tool. Rather, it is a general-purpose, object-oriented, multiplatform development environment that includes a powerful, dynamic language and a well-rounded class library. ScriptX is as applicable for implementing client/server applications as it is for authoring multimedia titles. While large, complex, and powerful, ScriptX is designed from the ground up in an integrated fashion, making it smaller, more consistent, and easier to learn than equivalent traditional systems (say, a C++ environment and class library).

With ScriptX's dynamic nature, classes, objects, and their relationships can be reconfigured during execution. Methods can be redefined and new objects added at run time. ScriptX code is semi-compiled into a bytecode representation, similar to that in Smalltalk, that is then interpreted by a platform-specific virtual-machine interpreter. Multithreading constructs are built into the language, which blends elements of Smalltalk, Dylan, Hypertalk, Lisp, Object Logo, C++, and Pascal.

Syntactically speaking, the language is small, about half the size of C and a fraction of C++ (in terms of BNF rules required to specify the grammar). There are no statements in ScriptX; every construct is an expression that returns a value. There are the usual block, iteration, and conditional constructs: If, Then, Else, While, and so on. There are also various ways of setting variable scopes: the local and global keywords for individual variables, and the module keyword which is a larger-granularity, scope construct roughly equivalent to the recently added namespace keyword in C++.

Everything in ScriptX is an object, including integers, strings, methods, classes, and functions. Integer and float objects have an "immediate" implementation that allows the bytecode interpreter to obtain values without method dispatching (by using tag bits and a bitshift, as is done in many Smalltalk implementations). There is no distinction at the level of bytecode representation between user-defined objects and classes and those supplied by the system; every entity has equivalent first-class status. In addition to supporting multiple inheritance, ScriptX lets you specialize behavior not just with classes, but also at the level of particular objects and individual methods. The ScriptX Core Classes comprise a class library that provides integrated general-purpose facilities like those in app frameworks such as OWL, MFC, zApp, MacApp, and C++/Views. The ScriptX library consists of about 240 classes and perhaps 2000 methods and provides support for text, fonts, streams, events, menus, scrolling lists, push buttons, scroll bars, files, properties, error-handling, arrays, B-trees, and more.

The most important aspect of the ScriptX classes is the use of classic Model-View-Controller (MVC) paradigm which, by decoupling an application's dataset from its presentation, enables the user to simultaneously view and manipulate an app's data in different ways (graphically, textually, and so on).

A key ScriptX feature is a Clock class, which provides facilities for synchronizing timed sequences of actions required by multimedia apps. Other classes implement a persistent-object storage facility, a search engine to retrieve objects within large-scale models, facilities for spawning and synchronizing multiple threads, a rich set of exception-handling mechanisms, and (in the future) support for distributing objects across networks.

For more information, send e-mail to [email protected].

Figure 1 A scene from Playfarm. The hand at the bottom of the image is an intelligent-cursor object, part of the user-representation component.

Figure 2 The layered structure of the Playfarm project distinguishes the modeling space from the presentation space.

Figure 3 The structure of a Playfarm character, which spans both modeling and presentation spaces.

Figure 4 The model space has an underlying triangular structure, allowing six directions of motion.

Listing One

------------------------------------------------
-- EXCERPTS FROM ANIMATION CLASS IN PLAYFARM
------------------------------------------------
class Animation(TwoDShape, Projectile, Pannable)
instance variables
    seq
    cell
    animationClock
    blockflag
    animationq
    basicset
    transitionset
    lasttransition
    partialpivots
    movecontroller
    modelshell
end

------------------------------------------------
method startAnimation self {class Animation} ->
(
     startClock self
     self.blockflag.state := @open
)
------------------------------------------------
method doaction self {class Animation} #key anim:(undefined) ->
(
     local allseqs
     if anim = undefined do 
     (
         print "animation called without a sequence"
         return false
     )
     -- if the animation is in the list of the last transitionset's 
     -- animations then add it to the queue
     if (ismember self.transitionset[self.lasttransition] anim) then
     (
         append self.animationq self.basicset[anim]
     )
     -- otherwise find the transition necessary to perform the animation by
     -- checking each transition list to see if animation is contained in it
     else
     (
         -- append the entire animation sequence returned to the queue
         local aq
         aq:= (buildAnimSequence self anim #(self.basicset[anim]))
         addmany self.animationq aq
     )
     self.blockflag.state := @closed 
     gateWait self.blockflag 
)
---------------------------------------------------------------------
-- This method recursively builds the sequence of animations required
-- to perform the requested animation since it was not possible to
-- perform the animation from the last transition.  anim is the
-- animation you want to perform seq is the array that gets built that
-- holds the entire sequence through recursion When a method (other
-- than this one) calls this method set seq:= #(anim)
method buildAnimSequence self {class Animation} anim seq ->
(
    local a, akey, atran
    -- get the transition that the animation can be performed from
    a:= select first array in self.transitionset where (ismember it anim)
    -- get the transition token
    akey:= getkeyone self.transitionset a.target
    -- if this transition is not the last transition, recurse to find 
    -- the transition that akey is a member of
    if akey <> self.lasttransition do
    (
        -- add it to the animation sequence
        prepend seq self.basicset[akey]
        buildAnimSequence self akey seq
        atran:= self.partialpivots[akey]

        if atran <> empty then

            self.lasttransition:= atran
        else
            self.lasttransition:= akey
    )
    seq
)
---------------------------------------------------------------
method tick self {class Animation} ->
(  
    if (self.seq = empty) do 
    (
           if ((self.seq := self.animationq[1]) = empty) do return
           self.velocity.x := self.seq.velocityx
           self.velocity.y := self.seq.velocityy
           -- If there is a sound for this frame sequence, play it.
           local s := self.seq.sound
           if (s <> undefined) do
           (
               -- Add time callback so player will stop and go to beginning.
               addTimeCallback        \
                      s (p -> (stop p; goToBegin p)) s #() s.duration false
               play s
           )
           self.seq
    )
    -- Change the stencil to the next animation cell in the series.
    self.boundary := self.seq[self.cell]
    tickle self.movecontroller self.animationClock
    
    self.cell := ((mod self.cell (size self.seq)) as Integer) + 1
    if self.cell = 1 do 
    (
        if (self.animationq[3] = empty) do 
        (
            self.blockflag.state := @open
        )
        setPresenterX self self.x + self.seq.extrax
        self.y := self.y + self.seq.extray
        deletenth self.animationq 1
        self.seq := empty
    )
)
------------------------------------------------
-- EXCERPTS FROM SHEEP PRESENTER CLASS
------------------------------------------------
class SheepPresenter (Animation, ModelDragger)
end
--------------------------------------------------------
method init self {class SheepPresenter} #rest args ->
(
     apply nextmethod self args
     self.x := 0

     self.y := 180
     self.velocity := new point x:0 y:0
     m := rootHandle "media" SheepStore
     -- Changed: added sound.
     local baaahSound := Sounds["sheep baaah friendly"]
     -- Changed: added sound.
     self.basicset := #(
       @walkright:     \
                 (makeFrameSequence m 1 1 #(151,152,153,154,155,156,157,158)),
       @walkrightdown: \
                 (makeFrameSequence m 2 1 #(151,152,153,154,155,156,157,158)),
       @knitright:     \
                 (makeFrameSequence m 0 0 #(163,164,165,166,167,168,169,170) \
                  sound:baaahSound),
       @exerciseleft:  \
                 (makeFrameSequence m 0 0 \
                  #(63,64,65,66,67,68,69,70,71,72,73,74)),
       --........and so on for the rest of the sequences....
     )
     self.transitionset := #(
       @turnleft: \
               #(@walkleft, @walkleftdown, @walkleftup, @measureleft, \
                 @exerciseleft, @complainleft, @tiltheadleft, @eatleft, \
                 @dropleft, @knitleft, @faceleft,@turnright),
       @turnright: #(@walkright, @walkrightup, @walkrightdown, \
                 @measureright, @exerciseright, @tiltheadright, @eatright, \
                 @dropright, @knitright, @faceright,@turnleft)
     )
     self.partialpivots:= #()
     self.lasttransition := @turnright
     self.boundary := m[151]
     return self  
)
------------------------------------------------
-- EXCERPTS FROM SHELL CLASS
------------------------------------------------
class Shell (rootObject)
instance variables
     attributes       -- the attributes of the shell, such as category, 
                      -- size, moveable
     shelltype        -- types in Playfarm are @prop or @character or @fixture
     homecell         -- the cell the shell is contained in now
     parentModel      -- the model grid 
     parentSpace      -- the presentation space        
     shellPresenter   -- the animation, or presentation object, for this shell
     direction        -- own direction, @left or @right are supported
     targetdirection  -- holder for the direction of another shell 
                      -- interacting with this one
     busy             -- boolean for busy state (when in a choreography
                      --  or being dragged a shell's busy state is true)
end
------------------------------------------------------------

-- This method adds a new shell to the model space and creates a
-- representation in presentation space.
method addToScene self {class Shell} model space ->
(
     self.parentModel := model
     self.parentSpace := space
     
     if (self.homecell <> undefined) then
     (
         reconcilecell self
         switchto self.homecell self
     )
     else
         reconcilexy self
     if (self.shellpresenter <> undefined) do
         prepend space self.shellpresenter
)
---------------------------------------------------------------
method celltoxy self {class Shell} ->
(
      -- translate the row/column coordinates to the x,y coordinates 
      -- in presentation space. 
     local node := self.homecell.location
     local x, y
     y := node[1] * NODEHALF + HORIZON - self.shellpresenter.height
     if (mod node[1] 2 = 0) then
     (    x := (node[2] * NODELENGTH \
                  - (self.shellpresenter.width / 2) - NODEHALF) as integer
     )
     else
     (    x := (node[2] * NODELENGTH \
              - (self.shellpresenter.width / 2) - NODELENGTH) as integer
     )
     setPresenterX self.shellpresenter x
     self.shellpresenter.y := y
)
----------------------------------------------------------------
-- reconcile presentation and model space coordinates based on
-- presentation coordinates. This method is typically called after a
-- shellpresenter is dragged and dropped.
method reconcilexy self {class Shell} #key x: y: ->
(
     if (x <> unsupplied) do
     (
         setPresenterX self.shellpresenter x
         self.shellpresenter.y := y
     )
     xytocell self
     celltoxy self  
)
------------------------------------------------
-- EXCERPTS FROM CHARACTER SHELL CLASS
------------------------------------------------
class CharacterShell (shell)
instance variables

     belongings           -- props (and possible other objects) the character 
                          -- is carrying
     charActivityEngine   -- the activity engine guiding this character
end
--------------------------------------------------------------
method init self {class CharacterShell} #rest args ->
(
     apply nextmethod self args
     self.belongings := #()
     self.shelltype := @character
     self.charActivityEngine := new ActivityEngine
)
--------------------------------------------------------------------
-- EXCERPTS FROM SHEEP CLASS
-- Specialized behavior for Irma La Sheep.  Activities and choreographies.
--------------------------------------------------------------------
class Sheep (CharacterShell)
end
method init self {class Sheep} #rest args ->
(
     apply nextmethod self args
     self.shellpresenter := new SheepPresenter \
                               actioneng:self.charActivityEngine
     self.shellpresenter.modelshell := self
     -- Wander Around Activity
     local wander := new CharacterWanderAction actor:self

     -- Knit Sweaters Activity
     local isAnimalNearBy := new AnimalNearbySensor pshell: self
     local measure := new MAction \
                  actor:self targetAction: self.shellpresenter \
                  anim: #(@measureleft,@measureright)
    -- function that creates a new sweater object. This function 
    -- is passed to the knit action as the effect
      local function knitfunc arg ->
      (
          local b := getnth \
                     #(propBitmaps["sweater 1"],propBitmaps["sweater 2"],\
                       propBitmaps["sweater 3"]) \
                       ((rand 3) + 1)
          local pp := new PropPresenter boundary:b fill:blackbrush \
                          stroke:undefined
          local p  := new PropShell propPresent: pp
          p.attributes := new SortedKeyedArray \
                          keys:#(@category, @size, @moveable) \
                          values:#(@item,@small, @true)
        return p
      )
      local knit := new ProduceAction actor:self \

                 effect: knitfunc \
                 anim: #(@knitleft,@knitright) \
                 targetAction: self.shellpresenter 
      local giveGift := new DropAction actor:self \
                    targetAction: self.shellpresenter \
                    anim: #(@dropleft,@dropright) 
      local hand_item := new MessageAction actor:self message: @hand_item \
                              effect: (arg -> \
                              arg.target := isAnimalNearBy.searchResult; \
                              arg.adata := giveGift.target)
      local knit_seq := new TwoShellChoreog \
                 seq: #(measure, knit, giveGift, hand_item) \
                 actor: self sensor:isAnimalNearBy
      local wander_loop := new LoopAction \
                  targetaction: wander condition: (-> ask isAnimalNearBy)
      local findAnimal2Knit :=  new ActionSequence actor:self \
                             seq: #(wander_loop, knit_seq, wander, wander)
      -- Complain Activity
      local complainAction := new MAction \
              actor:self targetAction: self.shellpresenter \
              anim: #(@complainleft,@complainleft)
      local randComplain := new randomAction \
                           actor:self targetAction:complainAction \
                           probability: 60
     local complainActivity := new ACtionSequence actor:self \
                     seq: #(wander, wander, wander, randComplain)
     -- The schedule for the sheep contains 2 activities:
     -- findAnimal2Knit and complainActivity The time slot
     -- each would run is 5 cycles. See comments in
     -- activeng.sx. This is a primitive implementation of
     -- schedules, will be upgraded.
     self.charActivityEngine.schedule := #(findAnimal2Knit,complainActivity)
     self.charActivityEngine.timeunit := 5
     self.charActivityEngine.timeCycle := 10
)
--------------------------------------------------------------------
-- EXCERPTS FROM ACTION CLASS
--------------------------------------------------------------------
class MAction (rootObject)        
instance variables
    actor      -- the character or prop that owns this action
    effect     -- a script to run after the actio is complete. 
               -- things like increasing or decreasing strengthners go here
    anim          -- token for animation to call
    targetAction  -- the presenter (or embeded action) associated with action
end
---------------------------------------------------------------------

method init self {class MAction} #rest args #key targetAction:(undefined) \
                                anim: actor: effect: ->
(    
    self.actor := actor
    self.targetAction := targetAction
    self.effect := effect
    self.anim := anim
)
-- This method carries out the targetAction. If target action is
-- an animation, it will animate.
method doAction self {class MAction} #key anim: ->
(
    if ((anim <> unsupplied) and (anim <> undefined)) do self.anim := anim
    if self.targetAction <> undefined do 
    (
      if self.anim <> unsupplied and self.anim <> undefined then
      (
        doAction self.targetAction \
        anim: (if (self.actor.targetdirection = @left) then \
               self.anim[1] else self.anim[2])
         )
         else
           doAction self.targetAction
       )
    if self.effect <> unsupplied do self.effect(self) 
)
---------------------------------------------------------------------
-- This action class encapsulates the behavior of taking an object
-- form the environemt and then owning (carrying) it.
class TakingAction (ActionWithOther)
end
method doAction self {class TakingAction} #key anim: ->
(
    nextmethod self anim:anim
    if self.actor <> undefined and self.target <> undefined do
    (
        -- Remove the prop form the environent and add to the actor.
        -- addProp self.actor self.target
        removeProp self.target.homecell self.target
        deleteOne self.target.parentSpace self.target.shellpresenter
    )
)
---------------------------------------------------------------------
-- This action is the complementary action to taking action. It
-- encapsulates the behavior of adding an object to the environment.
class DropAction (ActionWithOther)
end

method doAction self {class DropAction} #key anim: ->
(
    nextmethod self anim: anim
    

    if self.actor <> undefined and \
       ((getnth self.actor.belongings 1) <> empty) do
    (
        self.target := (getnth self.actor.belongings 1)
        deleteOne self.actor.belongings self.target
        self.target.homecell := self.actor.homecell

        addToScene self.target self.actor.parentModel \
                    theSceneSpace
        if ((getOne (attributesGetter self.target) @moveable) = @true) do
                append theDragController self.target.shellPresenter
    )
)

Copyright © 1994, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.