Version 1 of sprites on canvas test

Updated 2009-04-28 20:27:26 by slebetman

An Experiment

This code is rather long. It draws several sprites (configurable by modifying NUMBER_OF_SPRITES) and animate them. Currently on my 1.5GHz PC the canvas can comfortably handle up to 50 sprites at once. This does roughly 12 frames per second on my PC.

This is really a proof of concept rather than anything useful. This is roughly a straightforward translation of one of my javascript projects (which explains why I'm using dicthash [L1 ] and why the code looks weird).

#! /usr/bin/env wish

set NUMBER_OF_SPRITES 50

package require math::constants

proc dicthash {cmd args} {
  set path [split [string range $cmd 1 end] .]
  set varname [lindex $path 0]
  set path [lrange $path 1 end]

  #figure out if we need to re-base our "this"
  set hashindex -1
  for {set i 0} {$i<[llength $path]} {incr i} {
    set  key [lindex $path $i]
    if {[string match %* $key]} {
      set hashindex $i
      lset path $i [string trimleft $key %]
    }
  }

  if {$hashindex == -1} {
    upvar 2 $varname var
  } else {
    # We need to re-base "this". This is done by repointing $path and $var
    # So we need to save the path that leads to this nested dict: $prepath
    set prepath [lrange $path 0 $hashindex]
    set path [lrange $path [expr {$hashindex+1}] end]
    
    # Also need to save the base dict somewhere: $basedict
    upvar 2 $varname basedict
    set var [dict get $basedict {*}$prepath]
  }

  if {[llength $args] == 0} {
    return [dict get $var {*}$path]
  } else {
    set subcommand [lindex $args 0]
    set args [lrange $args 1 end]
    
    # check to see if $subcommand is a method:
    if {[catch {dict get $var {*}$path $subcommand} script] == 0} {
    
      # "this" magic:
      set body [lindex $script 1]
      if {![string match {upvar 1 var this;*} $body]} {
        set body "upvar 1 var this;$body"
        lset script 1 $body
        dict set var {*}$path $subcommand $script
      }
      set ret [apply $script {*}$args]
      
      # If we are rebased we need to merge $var back 
      # into the base dict since tcl has value semantics:
      if {$hashindex != -1} {
        dict set basedict {*}$prepath $var
      }
      return $ret
    } else {
      # built in operators:
      switch -- $subcommand {
        "=" {
          if {[llength $path]} {
            if {[llength $args]} {
              set ret [dict set var {*}$path {*}$args]
              
              # If we are rebased we need to merge $var back 
              # into the base dict since tcl has value semantics:
              if {$hashindex != -1} {
                dict set basedict {*}$prepath $var
              }
              return $ret
            } else {
              error "value not specified"
            }
          } else {
            error "key not specified"
          }
        }
        "+" {
          if {[llength $path] == 0} {
            return [dict merge $var {*}$args]
          } else {
            error "invalid dict merge"
          }
        }
        "+=" {
          if {[llength $path] == 0} {
            return [set var [dict merge $var {*}$args]]
          } else {
            error "invalid dict merge"
          }
        }
      }
    }
  }
  error "unsupported operation on $cmd"
}

if {[info proc dicthash.unknown] == ""} {
  rename unknown dicthash.unknown
  proc unknown {cmd args} {
    if {[string index $cmd 0] == "%"} {
      return [dicthash $cmd {*}$args]
    } else {
      dicthash.unknown $cmd {*}$args
    }
  }
}

math::constants::constants pi

proc angleFromDelta {dx dy} {
  set hyp [distanceFromDelta $dx $dy]
  set theta [expr {asin($dy/$hyp)}]
  if {$dx<0} {
    set theta [expr {$::pi-$theta}]
  } elseif {$dy<0} {
    set theta [expr {$::pi*2+$theta}]
  }
  return $theta
}

proc angleDiff {theta gamma} {
  expr {acos(cos($theta-$gamma))}
}

proc sectorFromAngle {angle numberOfSectors} {
  set sec [expr {round(($angle/(2*$::pi))*$numberOfSectors)}]
  if {$sec == $numberOfSectors} {
    set sec 0
  }
  return $sec
}

proc distanceFromDelta {dx dy} {
    expr {sqrt($dx*$dx + $dy*$dy)}
}

proc distance {xy1 xy2} {
    distanceFromDelta \
    [expr {[lindex $xy1 0]-[lindex $xy2 0]}] \
    [expr {[lindex $xy1 1]-[lindex $xy2 1]}]
}

set SPRITE_SECTORS 8

pack [canvas .c -bg white -width 600 -height 400] -fill both -expand 1

proc copySprite {src dst width height x y {composite set}} {
  set w [expr {$width-1}]
  set h [expr {$height-1}]
  set startx [expr {$x*$width}]
  set starty [expr {$y*$height}]
  set endx [expr {$startx+$width}]
  set endy [expr {$starty+$height}]
  $dst copy $src \
    -from $startx $starty $endx $endy \
    -to 0 0 $width $height \
    -compositingrule $composite
}

set IMAGE_COL 3
set IMAGE_ROW 8
set IMAGE_WIDTH 12
set IMAGE_HEIGHT 24
image create photo boy -data {
R0lGODlhJADAAMIEAAAHA8iicunUw/7//P///////////////yH5BAEKAAQALAAAAAAkAMAAAAP+
SKrQ+zBKCYIF1E3ScKzWBYGB94XmAgpsSopjCKvWAAiuxeJnsKccH45HC9iID9ARWNkhg60nQ3hj
OktJYevkBEKltx12dGVqvU0pbUtmjdfs2O/jFrw5Yi9+3g673BpZQ2p+cQ1lZCUtZopEFVU+d01p
gQwNVFM3JZV4jYSIll0faYRnbXZoOnFJfFOAd0FpsHuoDHkoRSWYMbhBFyQmwL0jHUkaxcYYyBvM
zc7P0BvL0dDAE9PEei+z1j3cMrDbjMNQWpLg4zNQNktr5sGReXBVSE3sVWv3T2GtX/TwUfDBWUWL
ID+CX8JpAbPQDCg43wAJcrKP4r6GrCjCONj+j1S9eD8c2blSj1KwKD+wFAoJj1KRg8cudHkDoqYx
jCo8VmqyyYqojP2+1AK6imdHIbtU1KmIIympMJJaqAPGBxjPHD1HXZqBzKaxZ9iIURvLLCzZa+SS
SeM0RUYqdbnephXHi5wmMRfd1p0qYMmAeiPrnDT3cYimk34BJ/7z0xU9mj4e55BHFAzFVEMnZnYl
UXPEWeUcPhSqkLItiwAfeszM0ZBGwAsdgRypUhWqWpPMqUQ5SJknqL55P/LFz+shqZB/ByO1CWjz
m0GhGhwt1HLvNklPZ9/jpw/cmgYvfP+lbSsaZeXB6jnLvj0HtjnXv1/fra62tL7moturUG/+Fv+5
TEUYZHgRWAdNOLDDWoIA/IVggw0uSI+EgzxYIWPRXXbKdKad1plSHSaE2WaclRbiQVFpCOJoKGL4
0woqhoLKYCR5MOGBJ6FiYUg8PNKFIeJBBtNGQ9J0yWbHAeLic5yRyJFPGQaWIm0uXsQbgrcxld1V
3RRnl0x52RiIVWx18J1886nn3prumWUJmmbV9x9o+aXTH350rfidGNzw+UeDewLap6B/fmgLcHQg
uqKT1BV0TmNNlgLpHqApSgeJtPRpqJ6ifRJUk5Ntxx2mRzLVnTFSMdRFjqfmBAiSkUBiI3JopATG
kcNRGhY/TC46Jk7ORcShk0h5lUmxQEH+qUsvwlRlypy+bYRClwDGZ+2103SAJpvcdtuem2leA1+d
FOBHLn92povuunMKCE5y+7UVbyfzalKvHfdGMsxdAPJrlxt74hsOwAML7NNcBIsmrKbEsojahlOa
1pqVH8L4YmCCQYfDSSjiNghKLTnlyXU+ppScse/NNEaSMwqSFVG9PnlKwFFS6aqUVR586lMSL4Uh
tbHWB15ePWElJhPmZbNWNdt6q5XT0IorX5zzxlUuXPmNV23WUYFDtNb/ej2YDLCRPXYIZaN9tg5/
qM2K2ErBPdDAbr/NNhcMDxteiKGV2zCL78hh6MSpSRwbljFyFFncKhqFoy954BaZbsrDCLdFyRf6
k1K0Xq7cOTzK5fQOhr2+9yyI27UoOIetasdh6iB711/rxGkdZ9JiwTeuWFAz3TR7cqol9dV4mhs8
1+q2e867WOlX7TaLy9vvu/DGA7rZAd6dS1FkR5/1SM2Hp8o4CMlCPkPjr/63pLA+PJB1jbXGVeCL
OouxYTfHfP/iK0mVoy6s+hHnVvUfYw3Nf5YA1ptipbFFVIYRNdMbxagAkNO5rjBXmlVTWDe/X+jL
Rig4ysuwNRUznYlpa/kdtxIAADs=
}

# Split image tile into individual sprites:
array set sprites {}
for {set x 0} {$x < $IMAGE_COL} {incr x} {
  for {set y 0} {$y < $IMAGE_ROW} {incr y} {
    set sprites(boy.$x.$y) [image create photo \
      -width $IMAGE_WIDTH -height $IMAGE_HEIGHT
    ]
    copySprite boy $sprites(boy.$x.$y) $IMAGE_WIDTH $IMAGE_HEIGHT $x $y
  }
}  

proc count {stepcount} {
  expr {($stepcount+1)%120}
}

# Character object:
set character {
  size 50
    x 100
    y 100
    vector {x 0 y 1}
    speed 3
    dest {x {} y {}}
    sprite {
    img boy
    tag ""
        x 0
        y 2
  }
    stepcount 0
    
    step {{} {
    if {[%this.dest.x] == {} || [%this.dest.y] == {}} {
      return 0
    }
    
    set x [%this.x]
    set y [%this.y]
    
    set dx [expr {[%this.dest.x]-$x}]
    set dy [expr {[%this.dest.y]-$y}]
    
    set distance [distanceFromDelta $dx $dy]
    
    if {$distance < [%this.speed]} {
      # Arrived at destination, stop moving.
      %this.dest.x = {}
      %this.dest.y = {}
      return 0
    }
    
    if {([%this.stepcount] % 5) == 0} {
      %this setVector $dx $dy
    }
    
    set xx [%this.vector.x]
    set yy [%this.vector.y]
    
    set xx [expr {$xx*[%this.speed]}]
    set yy [expr {$yy*[%this.speed]}]
        
    %this.x = [expr {$x+$xx}]
    %this.y = [expr {$y+$yy}]
    
    .c moveto [%this.sprite.tag] [%this.x] [%this.y]
    
    %this.stepcount = [count [%this.stepcount]]
        
    set walkstate [expr {[%this.stepcount] % 4}]
    switch -- $walkstate {
      0 {%this.sprite.x = 0}
      1 {%this.sprite.x = 1}
      2 {%this.sprite.x = 0}
      3 {%this.sprite.x = 2}
    }
    %this renderImg
        
    return 1
  }}
      
    setVector {{dx dy} {
    math::constants::constants pi
    
    set theta [angleFromDelta $dx $dy]
    
    %this.sprite.y = [sectorFromAngle $theta $::SPRITE_SECTORS]
    %this renderImg
    
    set gamma [expr {(double([%this.sprite.y])/$::SPRITE_SECTORS)*2*$::pi}]
    
    %this.vector.x = [expr {cos($gamma)}]
    %this.vector.y = [expr {sin($gamma)}]
  }}
    
    renderImg {{} {
    global sprites
    set s [%this.sprite]
    
    .c itemconfigure [%s.tag] -image $sprites([%s.img].[%s.x].[%s.y])
  }}
}

proc makeUnit {proto spec} {
  set unit [dict merge $proto $spec]
  
  set tag [.c create image [%unit.x] [%unit.y]]
  %unit.sprite.tag = $tag
  
  %unit renderImg
  
  return $unit
}

proc sortCanvas {} {
  # raise/lower units based on y-coordinates
  global units
  set ylist {}
  foreach x [array names units] {
    lappend ylist [list [%units($x).sprite.tag] [%units($x).y]]
  }
  set ylist [lsort -decreasing -real -index 1 $ylist]
  foreach x $ylist {
    set item [lindex $x 0]
    .c lower $item topitems
    .c addtag topitems withtag $item
  }
  .c dtag topitems
}

array set units {}
for {set x 0} {$x < $NUMBER_OF_SPRITES} {incr x} {
  set units($x) [makeUnit $character [list \
    x [expr {rand()*600}] \
    y [expr {rand()*400}]
  ]]
}

proc anim {} {
  global units
  foreach x [array names units] {
    %units($x) step
  }
  sortCanvas
  after 80 anim
}
anim

# periodically give people random destinations
proc motivate {} {
  global units
  foreach x [array names units] {
    if {rand() > 0.8} {
      %units($x).dest.x = [expr {rand()*600}]
      %units($x).dest.y = [expr {rand()*400}]
    }
  }
  after 1000 motivate
}
motivate

bind . <Key-c> {
  catch {console show}
}

enter categories here