Animating SketchUp Models with Ruby
SketchUp’s embedded Ruby is quite different from using Ruby in the normal fashion. I'll get several workarounds out of the way before proceeding with the core of this tutorial.
Sketchup-Ruby Peculiarities
SketchUp configures Ruby's load path ($:) to load scripts only from ~/Library/Application Support/SketchUp 5/Plugins. Vim doesn't handle the spaces in that pathname which was a deal-breaker for me. To get around this, I created a symbolic link in the Plugins directory to redirect SketchUp to NagumoLoader.rb in my Eclipse workspace, which is is a Subversion working copy at ~/Projects/workspace/NagumoSim on my system. To set this up:
cd ~/Library/Application Support/SketchUp 5/Plugins
in -s ~/Projects/workspace/NagumoSim/NagumoLoader.rb NagumoLoader.rb
SketchUp will now discover NagumoLoader.rb in its plugin directory and load it automatically whenever it starts.
The next obstacle is that SketchUp provides its own version of Ruby which is quite different from the version I use externally. In particular, ruby gems doesn't work so none of your gems are accessible:
> require 'rubygems'
Error: #<RuntimeError: /usr/local/lib/ruby/1.8/powerpc-darwin8.6.0/rbconfig.rb:7: ruby lib version (1.8.4) doesn't match executable version (1.8.0)>/usr/local/lib/ruby/1.8/powerpc-darwin8.6.0/rbconfig.rb:7
Finally, the embedded ruby is configured to load scripts only from the plugins directory. Therefore no classes are available except the Ruby built ins. NagumoLoader.rb works around this by extending the ruby load path. The first group of $: (load path) assignments add the standard Ruby load paths. The second group adds the various source directories that make up the NagumoSim project (src, src/Util, src/Things, src/People, src/Places, etc).
$defout = $stdout = File.new("/NagumoSim.log", "w")
$: << "/usr/local/lib/ruby/site_ruby/1.8"
$: << "/usr/local/lib/ruby/site_ruby/1.8/powerpc-darwin8.6.0"
$: << "/usr/local/lib/ruby/site_ruby"
$: << "/usr/local/lib/ruby/1.8"
$: << "/usr/local/lib/ruby/1.8/powerpc-darwin8.6.0"
$: << "."
dir = "/Volumes/HD2/Users/bcox/Projects/workspace/NagumoSim"
$: << "#{dir}/src";
$: << "#{dir}/src/Things";
$: << "#{dir}/src/People";
$: << "#{dir}/src/Places";
$: << "#{dir}/src/Util";
$, = ", " # define sensible field separator
begin
require 'Simulation'
rescue Exception
print $!
print $!.backtrace.join("\n")
rescue StandardError
print $!
print $!.backtrace.join("\n")
rescue RuntimeError
print $!
print $!.backtrace.join("\n")
rescue
print $!
print $!.backtrace.join("\n")
end
if( not file_loaded?("NagumoLoader.rb") )
add_separator_to_menu("Camera")
animation_menu = UI.menu("Camera").add_submenu("NagumoSim")
animation_menu.add_item("Start") {
@@simulation = Simulation.new
SketchUp.active_model.active_view.animation = @@simulation
}
animation_menu.add_item("Stop") {
@@simulation = nil
SketchUp.active_model.active_view.animation = nil;
}
animation_menu.add_item("--") { }
animation_menu.add_item("Idle") {
@@simulation.commander.Idle
}
animation_menu.add_item("Alert") {
@@simulation.commander.Alert
}
animation_menu.add_item("ArmForLandAttack") {
@@simulation.commander.ArmForLandAttack
}
animation_menu.add_item("ArmForSeaAttack") {
@@simulation.commander.ArmForSeaAttack
}
animation_menu.add_item("LaunchAttack") {
@@simulation.commander.LaunchAttack
}
animation_menu.add_item("RetrievePlanes") {
@@simulation.commander.RetrievePlanes
}
end
file_loaded("NagumoLoader.rb")
The begin...end block is the heart of this simulation. It demonstrates one solution to an uncertainty that has plagued this project from the beginning... exceptions don't work as you'd expect. The symptoms are that exception inheritance can't be relied on to catch Exception subclasses, and when such unhandled exceptions do occur, they crash SketchUp altogether. I think something is broken in how exceptions are passed between ruby and SketchUp's native language (C probably). My workaround is to rescue every exception that might occur explicitly as shown in this example, rather than by counting on exception inheritance to work correctly.
The rest of the file demonstrates SketchUp's technique for extending the SketchUp menu system with new commands. I've added a NagumoSim entry to the Camera menu with start and stop submenus, which call the start and stop methods respectively. The start menu simply creates a simulation instance and assigns it to SketchUp.active_model.active_view.animation This instructs SketchUp to call this instance's nextFrame method about once per second until the assignment is changed back to nil (by the stop menu). I have not found a way to control the frequency of these calls.
SketchUp provides nothing like RDT (ruby debugger toolkit for Eclipse), so you need to adjust to the inability to set breakpoints, examine variables, etc (it may be possible to use the Rails debugger; I've not tried this yet). Worse yet, the traditional debugging technique (scattering puts or print statements inside the source) won't work either because SketchUp redefines these as “private” methods for some reason and assigns $defout to the ruby console. This would be fine were it not for the fact that unhandled exceptions will crash SketchUp, taking the information you need to debug the problem with it. A work around is to override these methods in the following Object class, which redirects all debug output to a file via $stdout.
class Object
def puts s
$stdout.write " #{self}: #{s}\n";
end
def print s
$stdout.write " #{self}: #{s}\n";
end
end
$stdout and $defout are both redirected to a file by NagumoLoader. This ensures that crucial debug information is not lost if SketchUp crashes.
Simulation Loop
The Simulation class has only two methods, initialize and nextFrame. The initialize method builds the simulation and will be described later. The nextFrame method is automatically called by SketchUp and is shown below
def nextFrame view
begin
t1 = Time.now
print "time=#{@time} ================== #{t1-@t0}"
event = @commands[time]
if (event != nil)
print "event=#{event}"
catch :Blocked do
case event
when :OrderIdle
commander.Idle
when :OrderAlert
commander.Alert
when :OrderPrepareLandAttack
commander.PrepareLandAttack
when :OrderPrepareSeaAttack
commander.PrepareSeaAttack
when :OrderLaunchAttack
commander.LaunchAttack
when :OrderRetrievePlanes
commander.RetrievePlanes
else
print "Unrecognized event: #{event}"
end #case
end
end #if
Person.all.each { |p|
catch :Blocked do
p.Update
end
}
@rotationTests.each { |e| e.Update }
view.show_frame
@time += 1
@t0 = t1
$stdout.flush
rescue Exception
$stdout.print $!
$stdout.print $!.backtrace.join("\n")
rescue StandardError
$stdout.print $!
$stdout.print $!.backtrace.join("\n")
rescue RuntimeError
$stdout.print $!
$stdout.print $!.backtrace.join("\n")
rescue
$stdout.print $!
$stdout.print $!.backtrace.join("\n")
end
end
def time; @time; end
def crewQuarters; @crewQuarters; end
def officerQuarters; @officerQuarters; end
def deck; @deck; end
def bombTruck; @bombTruck; end
def torpedoTruck; @torpedoTruck; end
def munitionsOfficer; @munitionsOfficer; end
def flightOfficer; @flightOfficer; end
def hangar; @hangar; end
def tower; @tower; end
def bombs; @bombs; end
def torpedos; @torpedos; end
def dollys; @dollys; end
def planes; @planes; end
def crew; @crew; end
def officers; @officers; end
def dollyPark; @dollyPark; end
def commander; @commander; end
def name; "Simulation"; end
def to_s; name; end
def tests; @rotationTests; end
The key parts of this code are
•event = @commands[time]: Although events can also be generated from the menu system, it is convenient to drive the simulation automatically instead of via the menus. The Simulation class initializes @commands to contain a map of times and the event names (:OrderIdle, etc) to be issued at the corresponding time. All events are issued to 'commander', the Person instance with overall control of the airfield (i.e. Nagumo). The commander's state machine is programmed such that it will forward each event to each of his staff (FlightOfficer, MunitionsOfficer), and these will forward them to their staff (FlightCrew, MunitionsCrew), and so forth.
•Person.all.each...: Currently, all of the dynamic elements in this simulation are instances of Person. Person keeps track of the instances it has created and publishes them via it's Person.all method. The result of this code is that each Person receives an Update message each time SketchUp transfers control to its Ruby subsystem. This Update call is the basis for everything that will follow and thus the core of the event handling system.
•catch :Blocked: This intercepts :Blocked signals, which are thrown when agents (People) request resources that are temporarily unavailable. I'll describe this later.
•view.show_frame: Any changes made by the Ruby code are buffered in RAM. They are only drawn on the screen when your code calls view.show_frame.
•$stdout.flush: Text output is also buffered in ram. So this line ensures that output is flushed to the disk during each cycle.
•rescue ...: As explained elsewhere, exception hierarchies don't work, and missing one has severe consequences (SketchUp crashes). This code explicitly catches all exceptions that might conceivably arise.
•def time... This long list of accessor methods is a hack to make simulation's internal makeup accessible elsewhere. Its mainly useful in the Ruby console, but its occasionally accessed by other objects (via the simulation constructor argument) that need to access neighboring objects. For example, crew instances do this to locate crewQuarters. It would have been better to pass this via the constuctor. I went for the quick fix.
This is the other Simulation method. It constructs the Ruby objects that make up the simulation.
def initialize()
@t0 = Time.now
@time = 0
@tower = Tower.new(self,"Tower")
@tieDowns = []
4.times { |i|
tieDown = TieDown.new(self, "TieDown#{i+1}")
@tieDowns.push(tieDown);
}
@deck = Deck.new(self, "Deck", [], @tieDowns)
@dollys = []
4.times { |i|
dolly = Dolly.new(self,"Dolly#{i+1}")
@dollys.push dolly
}
@dollyPark = DollyPark.new(self, "DollyPark", @dollys)
@bombs = []
8.times { |i|
bomb = Bomb.new(self,"Bomb#{i+1}")
@bombs.push(bomb)
}
@bombTruck = Truck.new(self, "BombTruck", @bombs)
@torpedos = []
8.times { |i|
torp = Torpedo.new(self,"Torpedo#{i+1}")
@torpedos.push(torp)
}
@torpedoTruck = Truck.new(self, "TorpedoTruck", @torpedos)
@planes = []
4.times { |i|
plane = Plane.new(self, "Plane#{i+1}", [])
@planes.push(plane)
}
@hangar = Hangar.new(self,"Hangar", @planes)
@munitionsCrew = []
4.times { |i|
p = MunitionsCrew.new(self,"MunitionsCrew#{i+1}")
@munitionsCrew.push(p)
}
@munitionsOfficer = MunitionsOfficer.new(self,"MunitionsOfficer", @munitionsCrew)
@flightCrew = []
4.times { |i|
p = FlightCrew.new(self,"FlightCrew#{i+1}")
@flightCrew.push(p)
}
@flightOfficer = FlightOfficer.new(self,"FlightOfficer", @flightCrew)
@crewQuarters = Quonset.new(self,"CrewQuarters", @munitionsCrew+@flightCrew)
@officers = [@munitionsOfficer, @flightOfficer]
@officerQuarters = Quonset.new(self,"OfficerQuarters", @officers)
@commander = Commander.new(self, "Commander", @officers)
# Automated commands, to avoid having to select
# commands from menus during testing.
@commands = {}
@commands[5] = :OrderAlert
@commands[10] = :OrderPrepareLandAttack
@commands[20] = :OrderPrepareSeaAttack
@commands[60] = :OrderLaunchAttack
@commands[70] = :OrderRetrievePlanes
@rotationTests = []
4.times { |i|
t = RotationTest.new self, "RotationTest#{i+1}"
@rotationTests.push t
}
end
There little of interest here; just creating instances and connecting them to each other. The main thing to note here is the String arguments in each constructor ("MunitionsCrew#{i+1}", for example. These provide a way for each Ruby object to associate itself with a Sketchup instance. This is described in more detail in the SimObject Classes section.
By far the biggest change is the inversion of control that SketchUp imposes on Ruby. Other than snippets executed within the ruby console, SketchUp calls Ruby, not the other way around. This is why debuggers can't be used. It is also why most code changes require exiting SketchUp, restarting it, and relaunching your application (with rare exceptions due to the fact that you can, if you're careful, reload changed files via the ruby console). But the differences run much deeper than that. Most of us learned to program in the familiar procedural style. This means if you want your animation to do things in a certain order, you'd write each step as a ruby procedure and call them in order. For example, an animation of getting ready for work might consist of a goToWork procedure something like this:
def goToWork()
wakeUp()
getUp()
shower()
shave()
...
end
Then you'd define procedures for each activity (wakeUp, getUp, etc), then procedures for of the microactions within it (shutOffAlarmClock(), throwBackCovers), and so forth to the atomic actions that make each higher-level action up.
SketchUp requires an entirely different approach. You initiate an animation by registering an object with SketchUp.active_model.active_view.animation = anObject. SketchUp responds by sending a stream of nextFrame(view) messages to the object you specified. This instance must remember its state between calls and sequence its behavior accordingly.
The classic solution for problems of this nature is a state machine. There are many ways to build these, manually and otherwise. I started out by using SMC (state machine compiler), hoping that the automation would save time and be easier to extend over time. SMC has code generators for many languages (including ruby), and at first glance seems to be well-documented. However the documentation turned out to thin and superficial in the most critical of areas; how to manage the nesting of states implied by the goToWork example above. So I reverted to a more conventional, manual approach, which is mainly expressed in the Person and State classes.
require "SimObject.rb"
require "Que.rb"
require "State.rb"
class Person < SimObject
@@all = []
def initialize(simulation, state, name)
super(simulation, name)
@selfQ = Que.new("SelfQ", self, [])
@boss = nil
@@all.push self
@currentState = state
@previousState = nil
@globalState = nil
@arguments = []
end
def Person.all; @@all; end
def boss=(boss); @boss = boss; end
def boss; @boss; end
def selfQ; @selfQ; end
def Block; throw :Blocked; end
def Unblock; end
def Arguments; @arguments; end
# Public state management
def Update; @currentState.Execute self; end
def CurrentState; @currentState; end
def PreviousState; @previousState; end
def GlobalState; @globalState; end
# Private state management
def ChangeState newState, *arguments
@arguments = arguments
@previousState.Exit(self) if @previousState
@currentState = newState
@currentState.Enter self
end
def RevertToPreviousState; @currentState = @previousState; end
end
The Update and ChangeState methods are core of the state management machinery. Update, which s sent by the Simulation class during each simulation cycle, implements the Person's behavior while it is in a certain state (@currentState). Thus it call's the current state's Execute method. ChangeState, by contrast, handles transitions between states so it calls the current and next state's Enter and Exit methods respectively. These methods are defined in the State class and overridden in each of its subclasses as appropriate. Notice that ChangeState accepts a variable length argument list, which it makes accessible to subsequent code by saving them in the @arguments instance variable.
This is an abstract class that defines three class methods; Enter, Exit and Execute. The Enter and Exit methods are executed once when a state is entered and exited, respectively. The Execute method is called by the agent's Update method; about once per second. The "agent" is whatever instance the state applies to; in this example, the various instances of Commander, FlightOfficer, FlightCrew, MunitionsOfficer or MunitionsCrew.
# Since states are read-only and only have class methods,
# we must provide the agent as an argument to each method. The agent is
# the object whose state is being managed here.
class State
def initialize
end
def State.Enter agent
end
def State.Execute agent
end
def State.Exit agent
end
end
The Commander class is a simple example of how States are used. The Simulation initialize method constructs a single instance of this class to represent the airfield commander. I'll explain how instance initialization later to focus first on how the state machine works. The six methods after initialize (Idle, Alert, PrepareLandAttack, PrepareSeaAttack, LaunchAttack, and RetrievePlanes) are the interface between commander instances and the Simulation class. They represent the six commands that the Commander recognizes. These six events drive everything else.
require 'Officer'
class Commander < Officer
def initialize(simulation, name, staff)
super(simulation, Idle, name, staff)
end
def Idle; ChangeState Idle; end
def Alert; ChangeState Alert; end
def PrepareLandAttack; ChangeState PrepareLandAttack; end
def PrepareSeaAttack; ChangeState PrepareSeaAttack; end
def LaunchAttack; ChangeState LaunchAttack; end
def RetrievePlanes; ChangeState RetrievePlanes; end
class Idle < State
def Idle.Enter agent
agent.staffQ.resources.each { |e| e.Idle }
end
end #Class
class Alert < State
def Alert.Enter agent
agent.staffQ.resources.each { |e| e.Alert }
end
end #Class
class PrepareLandAttack < State
def PrepareLandAttack.Enter agent
agent.simulation.flightOfficer.PrepareLandAttack
agent.simulation.munitionsOfficer.PrepareLandAttack
#print agent.staffQ.resources
#agent.staffQ.resources.each { |e|
# print "#{e}.PrepareLandAttack"
# e.PrepareLandAttack}
end
end #Class
class PrepareSeaAttack < State
def PrepareSeaAttack.Enter agent
agent.simulation.flightOfficer.PrepareSeaAttack
agent.simulation.munitionsOfficer.PrepareSeaAttack
#print agent.staffQ.resources
#agent.staffQ.resources.each { |e|
# print "#{e}.PrepareSeaAttack"
# e.PrepareSeaAttack
#}
end
end #Class
class LaunchAttack < State
def LaunchAttack.Enter agent
agent.staffQ.resources.each { |e|
print "#{e}.LaunchAttack"
e.LaunchAttack}
end
end #Class
class RetrievePlanes < State
def RetrievePlanes.Enter agent
agent.staffQ.resources.each { |e|
print "#{e}.RetrievePlanes"
e.RetrievePlanes}
end
end #Class
end #Class
The rest of the Commander class consists internal classes that define the various States that the commander will be in as the simulation proceeds. Each is a subclass of State, and each defines one or more of the three State class methods methods. Each takes a single argument, agent, which is a reference to the commander instance. (This argument is the main cost of having made the State class read-only.) The method bodies use this to reference the commander instance which holds whatever instance information they need.
The munitions officer is a member of commander's staff who manages several instances of MunitionsCrew as his own staff. It publicizes methods (Idle, Alert, PrepareLandAttack, PrepareSeaAttack, etc) as methods. Each of these use ChangeState to advance its own state machine, which is implemented as internal subclasses of State.
require "Officer"
require "State"
class MunitionsOfficer < Officer
def initialize(simulation, name, staff)
super(simulation, Idle, name, staff)
end
def Idle; ChangeState Idle; end
def Alert; ChangeState Alert; end
def PrepareLandAttack; ChangeState PrepareLandAttack; end
def PrepareSeaAttack; ChangeState PrepareSeaAttack; end
def LaunchAttack; ChangeState LaunchAttack; end
def RetrievePlanes; ChangeState RetrievePlanes; end
class Idle < State
def Idle.Enter agent
agent.staffQ.resources.each { |e| e.Idle }
end
end #Idle
class Alert < State
def Alert.Enter agent
agent.staffQ.resources.each { |e| e.Alert }
end
end #Alert
class PrepareLandAttack < State
def PrepareLandAttack.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
crew = agent.staffQ.get(self)
crew.PrepareLandAttack plane
}
end
end #PrepareLandAttack
class PrepareSeaAttack < State
def PrepareSeaAttack.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
crew = agent.staffQ.get(self)
crew.PrepareSeaAttack plane
}
end
end #PrepareSeaAttack
class LaunchAttack < State
def LaunchAttack.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
if (plane.hasBombs? || plane.hasTorpedos?)
crew = agent.staffQ.get(self)
crew.LaunchAttack plane
end
}
end
end #LaunchAttack
class RetrievePlanes < State
def RetrievePlanes.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
crew = agent.staffQ.get(self)
crew.RetrievePlane plane
}
end
end #RetrievePlanes
end #MunitionsOfficer
MunitionsOfficer was relatively straightforward because its internal states were a direct reflection of its external command structure. MunitionsCrew is more complicated because most of its commands (PrepareLandAttack, for example) trigger multiple internal states (AcquireDolly, XferBombsStoreToDolly, XferBombsDollyToPlane, etc).
Some commands (PrepareLandAttack, PrepareSeaAttack) accept arguments (plane). FlightCommander provides these to tell the crew which plane to prepare Notice how some states (PrepareLandAttack, for example) are decomposed into finer-granularity states by assigning lists of sub-states to the @agenda variable in the macro-state's Entry method.Finally, this is the first class that shows how visual animations are handled.
For example, the Idle state is the military's "at ease' state. This state's Execute method is called once per clock tick (about 1/second). The at? method determines whether this crew member, identified by the agent argument, is within a stride length (@@strideLength class variable) of the simulation's crew quarters (agent.simulation.crewQuarters.origin). If so, the crew 'piddles', which is implemented (Person class) as a random walk at that location. Otherwise the stride method is called, which implements a stride (by @@strideLength) of the agent's visual representation towards that location.
require "Person.rb"
class MunitionsCrew < Person
def initialize(simulation, name)
super(simulation, Idle, name)
@selfQ = Que.new("SelfQ", self, [])
@agenda = []
@plane = nil
end
def commander; boss.commander; end
def selfQ; @selfQ; end
def plane; @plane; end
def SetAgenda agenda; @agenda = agenda; end
def NextAgenda; ChangeState @agenda.shift; end
def Idle; ChangeState Idle; end
def Alert; ChangeState Alert; end
def PrepareLandAttack plane; @plane = plane; ChangeState PrepareLandAttack, plane; end
def PrepareSeaAttack plane; @plane = plane; ChangeState PrepareSeaAttack, plane; end
def LaunchAttack plane; @plane = plane; ChangeState LaunchAttack, plane; end
def RetrievePlane plane; @plane = plane; ChangeState RetrievePlane, plane; end
class Idle < State
def Idle.Execute agent
if (agent.at?(agent.simulation.crewQuarters.origin))
agent.piddle
else
agent.stride(agent.simulation.crewQuarters.origin, agent.selfQ.resources)
end
end
end #Idle
class Alert < State
def Alert.Execute agent
if (agent.at?(agent.simulation.hangar.origin))
agent.piddle
else
agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
end
end
end #Alert
class PrepareLandAttack < State
def PrepareLandAttack.Enter agent
agent.SetAgenda [
AcquireDolly,
XferBombsStoreToDolly,
DumpTorpedos,
XferBombsDollyToPlane,
ReleaseDolly
]
end
def PrepareLandAttack.Execute agent
if (agent.at?(agent.simulation.hangar.origin))
agent.piddle
else
agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
agent.NextAgenda
end
end
end # PrepareLandAttack
class PrepareSeaAttack < State
def PrepareSeaAttack.Enter agent
agent.SetAgenda [
AcquireDolly,
XferTorpedosStoreToDolly,
DumpBombs,
XferTorpedosDollyToPlane,
ReleaseDolly
]
end
def PrepareSeaAttack.Execute agent
if (agent.at?(agent.simulation.hangar.origin))
agent.NextAgenda
else
agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
end
end
end #PrepareSeaAttack
class DumpBombs < State
def DumpBombs.Execute agent
plane = agent.plane
deck = agent.simulation.deck
if (agent.at?(plane.origin))
if (plane.hasBombs?)
munitions = plane.muntionsQ.get(self)
deck.deckQ.put(self, munitions)
end
agent.piddle
else
agent.stride(plane.origin, agent.selfQ.resources)
end
end
end # DumpBombs
class DumpTorpedos < State
def DumpTorpedos.Execute agent
plane = agent.plane
deck = agent.simulation.deck
if (agent.at?(plane.origin))
if (plane.hasTorpedos?)
munitions = plane.muntionsQ.get(self)
deck.deckQ.put(self, munitions)
end
agent.piddle
else
agent.stride(plane.origin, agent.selfQ.resources)
end
end
end #DumpTorpedos
class LaunchAttack < State
end #LaunchAttack
class RetrievePlane < State
end #RetrievePlane
# micro states
class AcquireDolly < State
def AcquireDolly.Execute agent
if (agent.at?(agent.simulation.dollyPark.origin))
dolly = agent.simulation.dollyPark.dollyQ.get(self)
agent.selfQ.put(agent, dolly)
agent.NextAgenda
else
agent.stride(agent.simulation.dollyPark.origin, agent.selfQ.resources)
end
end
end #AcquireDolly
class ReleaseDolly < State
def ReleaseDolly.Execute agent
if (agent.at?(agent.simulation.dollyPark.origin))
dolly = agent.selfQ.get(self)
agent.simulation.dollyPark.put(self, dolly)
agent.NextAgenda
else
agent.stride(agent.simulation.dollyPark.origin, agent.selfQ.resources)
end
end
end #ReleaseDolly
class XferBombsStoreToDolly < State
def XferBombsStoreToDolly.Execute agent
if (agent.at?(agent.simulation.bombTruck.origin))
bomb = agent.simulation.bombTruck.truckQ.get(self)
munitionsQ = agent.selfQ.resources[0].munitionsQ
munitionsQ.put(self, bomb)
agent.NextAgenda
else
agent.stride(agent.simulation.bombTruck.origin, agent.selfQ.resources)
end
end
end #XferBombsStoreToDolly
class XferBombsDollyToStore < State
def XferBombsDollyToStore.Execute agent
if (agent.at?(agent.simulation.bombTruck.origin))
munitionsQ = agent.selfQ.resources[0].munitionsQ
munitionsQ.get(self)
agent.simulation.bombTruck.truckQ.put(self, bomb)
agent.NextAgenda
else
agent.stride(agent.simulation.bombTruck.origin, agent.selfQ.resources)
end
end
end #XferBombsDollyToStore
class XferBombsDollyToPlane < State
end #XferBombsDollyToPlane
class XferBombsPlaneToDolly < State
def XferBombsPlaneToDolly.Execute agent
if (agent.at?(agent.plane.origin))
torpedo = agent.plane.muntionsQ.get(self)
munitionsQ = agent.selfQ.resources[0].munitionsQ
munitionsQ.put(self, torpedo)
agent.NextAgenda
else
agent.stride(agent.plane.origin, agent.selfQ.resources)
end
end
end #XferBombsPlaneToDolly
class XferTorpedosPlaneToStore < State
end #XferTorpedosPlaneToStore
class XferTorpedosStoreToDolly < State
def XferTorpedosStoreToDolly.Execute agent
if (agent.at?(agent.simulation.dollyPark.origin))
torp = agent.simulation.dollyPark.dollyQ.get(self)
munitionsQ = agent.selfQ.resources[0].munitionsQ
munitionsQ.put(self, torp)
agent.NextAgenda
else
agent.stride(agent.simulation.dollyPark.origin, agent.selfQ.resources)
end
end
end #XferTorpedosStoreToDolly
class XferTorpedosDollyToStore < State
def XferTorpedosDollyToStore.Execute agent
if (agent.at?(agent.plane.origin))
torp = agent.plane.munitionsQ.get(self)
munitionsQ = agent.selfQ.resources[0].munitionsQ
munitionsQ.put(self, torp)
agent.NextAgenda
else
agent.stride(agent.plane.origin, agent.selfQ.resources)
end
end
end #XferTorpedosDollyToStore
class XferTorpedosDollyToPlane < State
def XferTorpedosDollyToPlane.Execute agent
if (agent.at?(agent.plane.origin))
torpedo = agent.plane.muntionsQ.get(self)
munitionsQ = agent.selfQ.resources[0].munitionsQ
munitionsQ.put(self, torpedo)
agent.NextAgenda
else
agent.stride(agent.plane.origin, agent.selfQ.resources)
end
end
end #XferTorpedosDollyToPlane
end #
While MunitionsOfficer and MunitionsCrew are concerned with moving bombs or torpedoes to/from the Bomb and Torpedo trucks and planes, FlightOfficer and FlightCrew are concerned with moving planes from the hangar to the flight deck.
require "Officer"
require "State"
class FlightOfficer < Officer
def initialize(simulation, name, staff)
super(simulation, Idle, name, staff)
end
def Idle; ChangeState Idle; end
def Alert; ChangeState Alert; end
def PrepareLandAttack; ChangeState PrepareAttack; end
def PrepareSeaAttack; ChangeState PrepareAttack; end
def LaunchAttack; ChangeState LaunchAttack; end
def RetrievePlanes; ChangeState RetrievePlanes; end
class Idle < State
def Idle.Enter agent
agent.staffQ.resources.each { |e| e.Idle }
end
end #Idle
class Alert < State
def Alert.Enter agent
agent.staffQ.resources.each { |e| e.Alert }
end
end #Alert
class PrepareAttack < State
def PrepareAttack.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
crew = agent.staffQ.get(self)
crew.PrepareAttack plane
}
end
end #PrepareAttack
class LaunchAttack < State
def LaunchAttack.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
if (plane.hasBombs? || plane.hasTorpedos?)
crew = agent.staffQ.get(self)
crew.LaunchAttack plane
end
}
end
end #LaunchAttack
class RetrievePlanes < State
def RetrievePlanes.Enter agent
agent.simulation.hangar.planeQ.resources.each { |plane|
crew = agent.staffQ.get(self)
crew.RetrievePlane plane
}
end
end #RetrievePlanes
end #FlightOfficer
In this scenario, flight crew are responsible for moving planes between the hangar and tie down points on the flight deck.
require "Person.rb"
class FlightCrew < Person
def initialize(simulation, name)
super(simulation, Idle, name)
@selfQ = Que.new("SelfQ", self, [])
@agenda = []
@plane = nil
@tieDown = nil
end
def commander; boss.commander; end
def selfQ; @selfQ; end
def plane; @plane; end
def agenda; @agenda; end
def tieDown; @tieDown; end
def tieDown=(tieDown); @tieDown = tieDown; end
def SetAgenda agenda; @agenda = agenda; end
def NextAgenda; ChangeState @agenda.shift; end
# Externally-driven Transitions (Events)
def Idle; ChangeState Idle; end
def Alert; ChangeState Alert; end
def PrepareAttack plane; @plane = plane; ChangeState PrepareAttack, plane; end
def LaunchAttack plane; @plane = plane; ChangeState LaunchAttack, plane; end
def RetrievePlane plane; @plane = plane; ChangeState RetrievePlane, plane; end
class Idle < State
def Idle.Execute agent
if (agent.at?(agent.simulation.crewQuarters.origin))
agent.piddle
else
agent.stride(agent.simulation.crewQuarters.origin, agent.selfQ.resources)
end
end
end #Idle
class Alert < State
def Alert.Execute agent
if (agent.at?(agent.simulation.hangar.origin))
agent.piddle
else
agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
end
end
end #Alert
class PrepareAttack < State
def PrepareAttack.Enter agent
agent.SetAgenda [
GetPlaneFromHangar,
PutPlaneOnDeck
]
agent.NextAgenda
end
end
class LaunchAttack < State
end #LaunchAttack
class RetrievePlane < State
end #RetrievePlane
# micro states
class GetPlaneFromHangar < State
def GetPlaneFromHangar.Execute agent
if (agent.at?(agent.plane.origin))
agent.selfQ.put(agent, agent.plane)
agent.NextAgenda
else
agent.stride(agent.plane.origin, agent.selfQ.resources)
end
end
end #GetPlaneFromHangar
class PutPlaneOnDeck < State
def PutPlaneOnDeck.Enter agent
agent.tieDown=agent.simulation.deck.tieDownQ.get(self)
end
def PutPlaneOnDeck.Execute agent
if (agent.at?(agent.tieDown.origin))
plane = agent.simulation.hangar.planeQ.get(self)
agent.selfQ.put(agent, agent.plane)
agent.NextAgenda
else
agent.stride(agent.tieDown.origin, agent.selfQ.resources)
end
end
def PutPlaneOnDeck.Exit agent
agent.tieDown = nil
end
end #PutPlaneOnDeck
end #
The state machine logic emphasized so far is stable and unlikely to be changed further. Most of the still-unresolved issues are encapsulated in the SimObject and Que classes, which (along with several trivial intermediary classes, like Officer) defines key animation methods like piddle and stride. The class is in transition but important nonetheless: provides the animation methods via which ruby simulations interact with their SketchUp environment. The animation methods are undergoing active development. Many of them are experiments in progress and might well don't work.
require 'sketchup.rb'
require "Object.rb"
class SimObject < Object
@@visualDefinitionMap = {}
@@worldOriginPoint = Geom::Point3d.new(0,0,0)
@@worldOriginTranslation = Geom::Transformation.translation(@@worldOriginPoint)
@@zaxis = Geom::Vector3d.new(0, 0, 1)
def initialize(simulation, entityName)
@name = entityName
@simulation = simulation
Sketchup.active_model.active_entities.each { |e|
if(e.instance_of?(Sketchup::ComponentInstance) && e.name == entityName)
@visualAspect = e
return self
end
}
raise Exception, "Sketchup ComponentInstance #{entityName} not found"
end
def at?(destination, strideLength=100)
return (origin.distance(destination) < strideLength)
end
#angle = vector.angle_between visualAspect.transformation.xaxis
#rotation = Geom::Transformation.rotation origin, @zaxis, angle
#self.visualAspect.move!(rotation)
def stride destination, attachments=[], strideLength=30
vector = origin.vector_to destination
step = origin.offset vector, strideLength
# entities = Sketchup.active_model.active_entities
# entities.add_line(origin, step)
translation = Geom::Transformation.translation step
@visualAspect.move! translation
attachments.each { |a| a.visualAspect.move! translation }
end
def jumpTo origin
point = Geom::Point3d.new origin
translation = Geom::Transformation.translation(point)
@visualAspect.move!(translation)
end
def jumpTo(x, y, z)
point = Geom::Point3d.new(x, y, z);
translation = Geom::Transformation.translation(point)
@visualAspect.move!(translation)
end
def turnBy(radians)
origin = self.origin
rotation = Geom::Transformation.rotation origin, @@zaxis, radians
@visualAspect.move! rotation
end
def rotateBy(radians)
centerPt = @visualAspect.transformation.origin
rotation = Geom::Transformation.rotation(centerPt, @@zaxis, radians)
translation = Geom::Transformation.translation centerPt
@visualAspect.move!(rotation * translation)
end
def piddle(strideLength=100)
origin = self.origin
jumpTo(origin.x+rand(strideLength), origin.y+rand(strideLength), 0)
end
def origin; @visualAspect.transformation.origin; end
def model; @visualAspect.model; end
def bounds; model.bounds; end
def width; bounds.width; end
def depth; bounds.depth; end
def height; bounds.height; end
def center; bounds.center; end
def simulation; @simulation; end
def visualAspect; @visualAspect; end
def name; @name; end
def to_s; @name; end
end
First, notice that the initialize method takes two arguments. The simulation argument publicizes the simulation environment to all instances as a convenience feature useful for rapidly changing problems such as this. (This argument has since been replaced with a global parameter; $simulation, set by NagumoLoader.rb. The old way was making console printouts impossibly long since everything referenced everything else). The entityName argument is the name of the visual entity that represents this object in the SketchUp window. In other words, adding a new object involves three steps:
•Defining a Ruby class for that object (a subclass of SimObject) as described earlier.
•Using SketchUp's 3D drawing capabilities to create a visual representation for that object within the NagumoSim.skp file. Since these will often be replicated several times, it is usually wise to define each one as SketchUp component.
•Creating a uniquely named SketchUp instance for each object. I generally do this by creating as many visual instances as I'll need with the move/replicate tool. Then I use SketchUp's Outliner tool (Window/Outliner menu) in conjunction with the Entity Info (Window/Entity Info menu) to give each instance a unique name as shown below:
The need to preallocate visual instances like this at first feels awkward. But it soon feels liberating because it corresponds to how objects work in the real world. Thing's don't magically spring into existence during an initialize method, but are present (visually, but not otherwise) when SketchUp is first launched. Contrast this with the older approach (still visible in the SimObject.visualDefinition method in the above source). In the older approach, the Simulation class used this method to instantiate as many SketchUp component definition instances as it required, positioning each one via origin= calls. It proved far easier to use SketchUp's move tool to move preallocated instances than writing positioning commands as code.
Que.rb (resource conflicts)
The real world is automatically governed by the physical conservation laws that underlie resource conflicts and we 'handle' them naturally. But computers know nothing of physical conservation laws, so they must be explicitly dealt with in some manner. Obviously an airfield has only so many airplanes, bombs and torpedoes, and agents who request more than that must somehow be blocked until more are available. Less obviously, but conversely, the same truism is faced by those who deliver airplanes, bombs and torpedoes, in that the airfield has a finite amount of space to receive more. The same truism applies universally, to plane tie down spots on the tarmac, and even to individual crewmen, who must be explicitly provided the restriction that their hand's can only contain a single dolly, or that dolly can only hold so many bombs or torpedoes, and so forth.
Resource constraints are currently managed by the Que class, to be described below. Que is a corruption of Queue, chosen to avoid possible conflicts with other Queue classes I've experimented with. I'll rename it once I think of a better one.Almost every SimObject use Que's to hold (contain) resources. For example, Officers allocate Que's to "contain" the staff under their control, hangar's use Que's to "contain" planes, the FlightDeck uses Que's to "contain" tie down points, tie down points use Que's to "contain" whatever plane is tied down there. Even crew members use Que's to contain the tools in their hands.
For example, the first step in the MunitionsQueue agendas is AcquireDolly. This involves two separate Que operations, a "get" to check out a dolly from the DollyPark, and a "put" to add it to the crew member's internal Que. Both of these operations might in principle block; the get if no more dolly's are available in the park, and the put if the crew member's Que already contains a dolly. Here's how this is handled currently:
require 'sketchup';
# NagumoQueue: like a bread queue: some (fixed) number of loaves, some (predetermined, fixed)
# capacity for storing them, and two queues waiting to take loaves or leave them. A simple
# abstraction, but general enoough to support most of the resource limit problems in NagumoSim.
class Que
def initialize(name, attachedTo, initialResources=[], maxCapacity=1)
@name = name
@attachedTo = attachedTo
@maxCapacity = maxCapacity
@resources = initialResources
@waitingToPut = []
@waitingToGet = []
end
def name; @name; end
def attachedTo; @attachedTo; end
def origin; @attachedTo.origin; end
def isEmpty?(); @resources.size() == 0;; end
def isFull?(); @resources.size() >= @maxCapacity-1;; end
def maxCapacity; @maxCapacity; end
def resources; @resources; end
def waitingToPut; @waitingToPut; end
def waitingToGet; @waitingToGet; end
def resourceCount(); @resources.size;; end
def emptyCount(); @maxCapacity-@resources.size;; end
# Get single resource
def get(getter)
# print "#{getter}.get(#{self}) [#{resources}]";
n = 1;
if (@resources.size < n) # Insufficient resources
# print(" #{getter}.get blocked at #{self} [#{resources}]")
@waitingToGet.push(getter)
# getter.Block() # getter blocks
# raise BlockException, "#{getter} blocked"
throw :Blocked
else
return @resources.pop();
end
putter=@waitingToPut.pop();
putter.Unblock() if (putter != nil)
end
def put(putter, resource)
# print "#{putter}.put(#{resource}) [#{resources}]";
n = 1;
if (@resources.size+n > maxCapacity)
# print("#{putter}.put(#{resource}) blocked at #{self} [#{resources}]");
@waitingToPut.push(putter)
# putter.Block() # putter blocks
# raise BlockException, "#{putter} blocked"
throw :Blocked
else
@resources.push(resource);
end
if nextGetter = @waitingToGet.pop
nextGetter.Unblock()
end
end
def getArray(sender, n=1)
# print "getArray(#{sender}, #{n}))";
if (@resources.size < n)
# print("#{sender} blocking getting #{n} from #{self} (resources=#{resources.size()}")
@waitingToGet.push(sender)
sender.Block() # sender blocks
# raise BlockException, "#{putter} blocked"
throw :Blocked
else
resources = @resources[0..n-1]
resources.each { |e| @resources.delete(e) }
return resources
end
putter = @waitingToPut.pop();
putter.Unblock() if (putter != nil)
end
def putArray(sender, resourceArray, n=1)
# print "putArray(#{sender}, #{resourceArray}, #{n})";
if (@resources.size+n >= maxCapacity)
# print("#{sender} blocks putting #{n} to #{self} (resources=#{resources.size()}");
@waitingToPut.push(sender);
sender.Block();
else
resourceArray.each { |r| @resources.push(r); }
end
nextGetter = @waitingToGet.pop()
nextGetter.Unblock() if (nextGetter != nil)
end
end
Blocking is handled by raising a :Blocked signal to prevent further progress of the Update invocation for the currently executing object. This code may be OK, but the Unblocking operation is almost certainly wrong.
Note added June 7 2006: The solution for blocking/unblocking turned out to be so simple I'd overlooked it until I began coverting the simulation from Ruby to Java (as described in the last section). As I've not updated the Ruby code, the following Java example will have to do. The new Queue class is far simpler, little more than an ArrayList of resources and an maximum number of resources to store there. The modified version simply throws a ResourceConstraintException if either resource limit (too few or too many) is exceeded. The exception is caught by the State subclass and handled as shown here:
private class XferBombsFromStoreToDolly extends State
{
protected void enter()
{
turnToFace(World.instance.bombTruck);
}
protected void execute()
{
if (intersects(World.instance.bombTruck))
{
try
{
Dolly agentDolly = (Dolly) loadQ.resources.get(0);
xferResource(Bomb.class, World.instance.bombTruck.loadQ, agentDolly.loadQ);
advanceAgenda();
}
catch (ResourceConstraintFault e)
{
log.info(e+"");
}
}
else
moveTowards(World.instance.bombTruck);
}
protected void exit() {}
}
Paraphrasing, in the state of getting bombs from the bomb store, if you're not at the bombTruck (the intersects test), continue moving towards the truck (the moveTowards call). Once you are there, attempt to transfer a bomb from the truck to the dolly, counting on the truck's queue to throw a ResourceConstraintFault if there are no more bombs on the truck or if the dolly won't accept any more. If the transfer succeeds, advance to the next state on the agenda. If the transfer fails for either reason, remain in the current state until the resource constraint is relieved by the activities of other agents. The pattern shown here is repeated almost verbatim in all States that access constrained resources.
Translation/Rotation Transformations
The second obvious shortcoming is that the animation methods (of the SimObject class) handle translations but not rotations. This makes figures move like cardboard cutouts, without ever turning to face their destination. This is because SketchUp's documentation never explain how to combine translation and rotation transformations. I'm familiar with how to do this in OpenGL, but SketchUp's way escapes me. I'll document it here once I figure it out myself.These are just notes on what I've tried, mainly to keep track this for my own purposes.
Consistent observation is that combinations of translations with rotations don't work. Rotations work. So do translations. But doing one then the other erases the former. Visual effect is that rotating an object positioned over yonder makes it jump to the origin and rotate (and/or translate) from there.
A promising idea which failed to... manually build a transformation that combines the two operations. But print statements show that the translation info (in the 4th column) is simply erased during Geom::Transformation.new.
# Does not work. The Geom::Transformation.new tv returns a transformation
# whose origin is 0,0,0. A complete mystery.
# transformation = Geom::Transformation.translation offset
def stride1 destination, attachments=[], strideLength=100
vector = origin.vector_to destination
offset = origin.offset vector, strideLength
radians = vector.angle_between visualAspect.transformation.xaxis
print "origin=#{origin}\ndestination=#{destination}\nvector=#{vector}\noffset=#{offset}\nradians=#{radians}"
cos = Math.cos radians
sin = Math.sin radians
print "dx=#{origin.x+offset.x} dy=#{origin.y+offset.y} z=#{origin.z+offset.z}"
tv = [
cos, sin, 0, origin.x+offset.x,
-sin, cos, 0, origin.y+offset.y,
0, 0, 1, origin.z+offset.z,
0, 0, 0, 1,
]
print "tv=#{tv}"
transformation = Geom::Transformation.new tv
print "transformation=#{transformation.to_a}"
@visualAspect.move! transformation
print "va.transformation=#{@visualAspect.transformation.to_a}"
attachments.each { |a| a.visualAspect.move! translation }
end
The usual solution is to combine transformations by multiplying them like this, where T is the translation matrix and R is the rotational one: T-1 * M * T. Sketchup is doing the math OK insofar as I could check via Excel. But the net effect is that everything translates either to the origin to way off in left field. Here's the latest shot at it:
# The traditional solution: inverse * rotate * translate
def stride destination, attachments=[], strideLength=100
vector = origin.vector_to destination
offset = origin.offset vector, strideLength
print "origin=\n#{origin} dest=\n#{destination} vector=\n#{vector} offset=\n#{offset}"
translation = Geom::Transformation.translation offset
printMatrix "translation", translation
inverse = translation.inverse
printMatrix "translation", translation
radians = vector.angle_between visualAspect.transformation.xaxis
axis = @@zaxis
point = offset
rotation = Geom::Transformation.rotation point, axis, radians
transformation = inverse * rotation * translation
printMatrix "transformation", transformation
status = @visualAspect.move! transformation
printMatrix "result", @visualAspect.transformation
attachments.each { |a| a.visualAspect.move! transformation }
end
Finally, a real clue!! The hand-built transformation idea was right. The only thing wrong is that Sketchup's storing the transform of the usual transformation matrix; i.e. in column-major format. This version translates+rotates almost correctly, but with a 15 degree oscillation in the rotation each step that still needs to be debugged. Once that's sorted, I need to split out the rotation stuff to a separate rotate method. Only need to rotate during Entry/Exit methods, not every Execute as now. Still its about as fast as before even now.
def stride destination, attachments=[], strideLength=100
vector = origin.vector_to destination
offset = origin.offset vector, strideLength
radians = vector.angle_between visualAspect.transformation.xaxis
print "origin=#{origin}\ndestination=#{destination}\nvector=#{vector}\noffset=#{offset}\nradians=#{radians}"
cos = Math.cos radians
sin = Math.sin radians
print "dx=#{origin.x+offset.x} dy=#{origin.y+offset.y} z=#{origin.z+offset.z}"
tv = [
cos, -sin, 0, 0,
sin, cos, 0, 0,
0, 0, 1, 0,
offset.x, offset.y, offset.z, 1
]
printMatrix "tv", tv
transformation = Geom::Transformation.new tv
printMatrix "transformation", transformation
@visualAspect.move! transformation
printMatrix "result", @visualAspect.transformation
attachments.each { |a| a.visualAspect.move! transformation }
end
The uncertainties in the last section plus the slow frame rates I'd been experiencing made me reconsider the Schetchup and Ruby approach for this application. I've returned, at least experimentally, to the Java implementation I'd started with. I'll do a similar tutorial for this environment once it settles down a bit.
The new strategy is to leverage only Sketchup strengths as a 3D drawing environment, exporting models as .obj and .mtl files to a real game engine. Before I began exploring the Ruby approach, I'd tried JME (Java Monkey Engine), which uses LWJGL (Lightweight Java GL) to connect to the GL hardware. Although Sketchup is better documented than JME, the far side of the documented API is invisible, undocumented, and proprietary. For example, the reasons that some exceptions crash SketchUp, how translations/rotations are handled, and why performance seems limited to about 1 frame/second is still a mystery after weeks of determined digging for such information.
So with about a day's work, I converted all the Ruby code to Java, making major cleanups along the way. Although JME's documentation still sux, full source is available, there's an active and helpful user community, and the Eclipse debugger means everything is open to inspection. Plus I seem to getting a 200 fold speed improvement (200 fps) over SketchUp at the moment.
The main problem is that models imported from Sketchup as .obj and .mtl files are rendered as ugly black and white shapes. After considerable help from JME's key developers, they've been unable to explain it either. But I've since confirmed that SketchUp's materials (.mtl files) are imported successfully by Blender, which is proving very useful in understanding still-murky matters like material, textures, coordinates, and coordinate transforms.