examples/dynamics1.py

This is one of the example scripts included with Shady. These scripts can be run conventionally like any normal Python script, or you can choose to run them as interactive tutorials, for example with python -m Shady demo dynamics1

# $BEGIN_SHADY_LICENSE$
# 
# This file is part of the Shady project, a Python framework for
# real-time manipulation of psychophysical stimuli for vision science.
# 
# Copyright (c) 2017-2022 Jeremy Hill, Scott Mooney
# 
# Shady is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/ .
# 
# $END_SHADY_LICENSE$

#: High-level tools for directing the action (procedural version) 
"""
This demo shows how to use the bundled Shady.Dynamics submodule
to easily create properties that evolve over time, and how to
glue different dynamics together with a state machine to switch
between different discrete groups of stimulus behaviors.

Most of the demo will walk you through the setup of the state
machine and its states, before running it at the end.

Note that this demo shows a procedural style of implementing
the logic of a state machine.  Its sister demo, dynamics2, shows
the contrasting object-oriented approach for doing the same thing.

"""#.
if __name__ == '__main__':

	"""
	Let's start by creating a World, configured according to
	whatever command-line arguments you supplied.
	"""#:
	import Shady
	from Shady.Dynamics import StateMachine, Integral, Transition
	
	cmdline = Shady.WorldConstructorCommandLine()
	cmdline.Help().Finalize()
	Shady.Require( 'numpy', 'Image' ) # die with an informative error if either is missing
		
	
	world = Shady.World( **cmdline.opts )
	
	"""
	We'll use our included example image, `alien1`, as a stimulus.
	Note that we're telling Shady to load all of the individual frame
	images from the `alien1` folder, not the `.gif` file.
	
	We'll start our alien at the left end the world.
	"""#:
	alien = world.Stimulus(
		Shady.PackagePath( 'examples/media/alien1/*.png' ),
		x = -world.width / 2,
	)
	
	"""
	Now we'll start constructing the pieces of our dynamic world.

	We want our alien to stand, run, jump, fall, in that order.
	He will also spiral out of control if you press a certain key.
	These states will be fairly simple: the running and spiraling
	states will be triggered by user key presses, and the will
	all run on timers (runnng will lead to jumping which will
	lead to falling, although a key-press will also speed up the
	jump).
	
	Shady's `StateMachine` class can handle all most of this
	automatically by stringing states together in a certain
	order, but if we want a state to have multiple possible
	next states, we'll need to define the logic ourselves,
	as we shall see.
	"""#:

	"""
	We'll start by defining the state machine, which we imported
	from the Shady.Dynamics module.
	"""#:
	sm = StateMachine()
	"""
	Let's add our five states one by one. We'll define what
	happens in each state later. Right now, we want to set up
	how the *changes* between states will work.
	
	First, we add the 'stand' state, already alluded to in
	the function above. We tell the state machine the name of
	the state, and also tell it which state (yet to be defined)
	will come after this state in the default ordering of the
	machine.
	"""#:
	sm.AddState( 'stand', next='run' )
	"""
	We will define 'run' next. Same thing, but we're also
	giving this state a duration in seconds before it
	automatically moves to the next state.
	"""#:
	sm.AddState( 'run',  duration=2, next='jump' )
	"""
	Same for 'jump' and 'fall'.
	"""#:
	sm.AddState( 'jump', duration=0.4, next='fall' )
	sm.AddState( 'fall', duration=1.0, next='stand' )
	
	"""
	For the 'spiral' state, instead of simply specifying
	the name of its subsequent state as a string literal,
	we'll define `next` as a *function* that outputs the
	name of the next state. The state machine will run
	this function, passing an instance representing the
	current state (whose name will be 'spiral') as its 
	argument. The function's output will determine
	the alien's fate at that time.
	
	The function checks how long the state has been
	running (`.elapsed`). If it's less than three
	seconds, the state machine will cancel the request
	to exit the state (built-in constant `CANCEL`).
	 
	Otherwise, it returns a string, which will be used as
	the name  for the next state. (If that state doesn't
	exist, an error will occur).
	
	"""#:
	def ExitSpiral( state ):
		if state.elapsed < 3:
			alien.frame = Transition( 0, 24, duration=0.1 ) # wiggle
			return StateMachine.CANCEL
		return 'stand'
		
	sm.AddState( 'spiral', next=ExitSpiral )
	
	"""
	We've defined the skeleton of our state machine,
	but none of the actual behavior associated with its
	states.
	
	We'll give our stimulus an animation callback, which
	is just a function that is called by Shady on every
	frame before drawing this stimulus. The animation
	callback takes the stimulus itself as its first argument
	and the stimulus time (by default: seconds since World
	creation) as as its second argument.
	"""#:
	
	"""
	Here is it
	"""#:
	@alien.AnimationCallback
	def RunningJump( self, t ):
				
		# Calling the StateMachine instance with time `t` is what actually causes
		# the state-transition logic and management to run. It returns an instance
		# of a `State` object:
		state = sm( t )
		
		# The `State` instance has an attribute `.fresh` which indicates whether
		# the current time `t` is the earliest time for the current state. It is
		# a handy way of implementing state onset behaviors procedurally.
		if state.fresh:
		
			# Now we'll just check which state we're in and change the alien
			# accordingly. State instances can be compared directly to strings
			# to check their names.
			if state == 'stand':
				self.xy = 0   # stop any dynamic attached to the .xy property
				self.Set( x=-world.width/2 + 100, y=0, frame=0 ) # re-position him
				
			# So far so static.  In the other states, we'll use more `Shady.Dynamics`
			# tricks---in particular, the `Integral` with respect to time.
			if state == 'run':
				self.Set( x=Integral( 400 ) + self.x, frame=Integral( 16 ) )
				
			# A double-integral of a constant will give us the constant acceleration
			# that you would get from gravity, in both the 'jump' and 'fall' states
			# (let's say gravitational acceleration is 5000 pixels per second per
			# second).  When he jumps, let's say our alien achieves an initial
			# upward velocity of 1500 pixels per second.
			if state == 'jump':
				self.Set(y=Integral(Integral(-5000) + 1500) + self.y, frame=0)				
			if state == 'fall':
				self.frame = Integral(160)  # heeeeeeelp
			
			# Finally we'll use non-linear transformations of an `Integral` to achieve
			# a faster-and-faster spiralling effect in the 'spiral' state:
			if state == 'spiral':
				radius = min( world.width, world.height ) / 2 - 100
				self.Set( x=-radius, y=0, frame=0 )  # stop any .x, .y and .frame dynamics
				self.xy = Integral( Integral( 0.1 ) ).Transform( Shady.Sinusoid, phase_deg=[ 270, 180 ] ) \
				        * ( radius - Integral( 20 ) ).Transform( max, 0 )
	
	"""
	Our alien will do things now, but we haven't yet implemented
	a way of getting him started, since there's no condition to
	terminate the first 'stand' state. We'll hook into Shady's
	`EventHandler` mechanism.
	
	Like the `AnimationCallback`, this can be done using a
	decorator on our function definition. The function should
	take two arguments: the `Shady.World` being hooked into
	(here, written as `self`) and the event that will be handled,
	which contains data about mouse and keyboard input.
	"""#:
	@world.EventHandler
	def KeyboardControl( self, event ):
	
		# Press space to start running, to jump, or to exit from a spiral:
		if event.type == 'text' and event.text == ' ':
			if sm.state in [ 'stand', 'run', 'spiral' ]:
				sm.ChangeState() # without args: change to the `.next` state
		
		# Press enter to start spiralling:
		if event.type == 'key_release' and event.key in [ 'enter', 'return' ]:
			sm.ChangeState( 'spiral' )  # change to this explicitly named state
				
		# Press q or escape to stop and close the window:
		if event.type == 'key_release' and event.key in [ 'q', 'escape' ]:
			self.Close()
	"""
	And that's it - the alien is away! Try using spacebar and
	enter key to manipulate his state.
	
	Remember that we designed `ExitSpiral` to ensure that you
	cannot get out of a spiral until you have been in it for
	at least 3 seconds.
	"""#>
	Shady.AutoFinish( world )