Hi _why, On Nov 17, 2008, at 12:13 PM, _why wrote:
Hasty. This turns out to be 0.r1091, folks. _why
Looking good! Alas, my new-and-improved tankspank seems to run even *slower* now:
# Tankspank version 5.3, 25-July-2008 # # This code is hereby released into the public domain # http://creativecommons.org/licenses/publicdomain/ # # Originally by kevin conner <[EMAIL PROTECTED]> # # Updated by Ernest Prabhakar <[EMAIL PROTECTED]> # Notable Features: # * Abstract classes "Thing" for static objects, "Sprite" for moving objects # * Damage, drag both calculated dynamically based on size # * Converts between "game" and "screen" coordinates using moving Camera # * Use isometric projection (draw_box) for 2.5D rendering # * Miniature "Radar" display to help locate enemy tanks # * AppTest proxy object allows sanity-checking outside Shoes runtime require 'matrix' require 'logger' # # Parameters # $log = Logger.new(STDERR) #$log.level = Logger::DEBUG $log.level = Logger::INFO X=0 Y=1 N_ENEMIES = 5 #5 FRAMES_PER_SECOND=20 IMPULSE = 10.0/FRAMES_PER_SECOND TIGHTNESS = 0.1 # # Extensions to built-in Ruby classes # class TankVector < Vector def self.a2s(array) array.inject("[") {|p,q| p += sprintf(" %.1f",q)} + "]" end def self.[](*array); super; end def self.area(v); 4*v[X]*v[Y]; end def self.within?(v, size) v.each2(size) { |self_i, size_i| return false if self_i.abs > size_i } true end def self.rotation(angle) cos_a, sin_a = Math.cos(angle), Math.sin(angle) Matrix[ [cos_a, -sin_a], [sin_a, cos_a] ] end def self.reflect(v); TankVector[v[X], -v[Y]]; end def -(v); super; end def rotate_by radians; TankVector.rotation(radians) * self; end def reflect_by radians; TankVector.reflect(self).rotate_by(radians); end def ratio; @elements[Y] / @elements[X]; end def to_s; sprintf("[ %.1f, %.1f]", @elements[X], @elements[Y]) end end # # CONSTANTS # BOUNDS = TankVector[1500.0, 1250.0] SCREEN_SIZE = TankVector[700.0, 500.0] SCREEN_ORIGIN = SCREEN_SIZE * 0.5 Z_SCALE = 600.0 RADAR_SCALE = 0.1 EXPLODE_RATE = 3.0/FRAMES_PER_SECOND DRAG_MAX = 0.20 SHELL_SPEED = 20 SHELL_SIZE = TankVector[2.0,2.0] TANK_SIZE = TankVector[10.0,15.0] MUZZLE_SIZE = TankVector[25.0,0.0] AIM_RADIUS = 10 DEST_RADIUS = 20 HEADLIGHT_RADIUS = 3 BOUNCE_DISTANCE = 60.0 # # Mixin Modules # module Drawable def self.rand_color color = (1..3).collect { 0.2 + 0.4 * rand } color << 1.0 $app.rgb *color end def draw_oval; $app.oval *base_extent; end def draw_rect; $app.rect *base_extent; end def draw_rect_with_corners screen_corners base = screen_corners[2] extent = screen_corners[0] - screen_corners[2] $app.rect *(base.to_a + extent.to_a) end def draw_poly(points) points.inject(points[-1]) do |p, q| $app.line *(p.to_a + q.to_a); q end end def draw_outline points = screen_corners() draw_poly(points) end def draw_along(vector) p = screen_coord() q = p + vector $app.line *(p.to_a + q.to_a) end def draw_box bottom = screen_corners() top = screen_corners(@height) $log.debug "#{$camera}" $log.debug "\nbottom| #{bottom.to_s}\n top| #{top.to_s}" draw_outline 4.times { |i| $app.line *(bottom[i].to_a + top[i].to_a) } draw_rect_with_corners top end def draw $log.debug "\nDrawing #{self}" $app.stroke @stroke_color $app.fill @fill_color draw_content end def draw_content; draw_rect; end end module Hurtable attr_reader :health def reset_health; @health = mass(); end def strength; @health / mass(); end def dead?; @health <= 0; end def hurt damage; @health -= damage; end def mass; TankVector.area(@size); end def energy; 0; end # speed is zero def encounter obstacle # Sprites only damage = [energy(), obstacle.energy()].max hurt damage obstacle.hurt damage collide(obstacle) if mass < obstacle.mass end end # # Static "Things" # class Thing include Drawable include Hurtable attr_accessor :center, :size, :stroke_color, :fill_color def initialize(center, size) @center = center.clone() @size = size @height = @size.r * (0.25 + rand * 1.5) @dz = 1 + (2/Math::PI) * Math.atan(@height / Z_SCALE) @stroke_color = @fill_color = Drawable::rand_color reset_health() end def to_s s = "#{self.class}:[EMAIL PROTECTED]@size.to_s}" s += sprintf(" x %.0f | ", @height) s += "fill: [EMAIL PROTECTED] stroke: [EMAIL PROTECTED]" end def front; @height * @size[X]; end def volume; @height * area(@size); end def screen_coord(dz=1); SCREEN_ORIGIN + (@center - $camera.center) * dz; end def base_extent; (screen_coord - @size).to_a + (@size * 2).to_a; end def native_base_extent; (@center - @size).to_a + (@size * 2).to_a; end # Center +- sizes/ reflected size def cornerize(ctr, s, sR); [ctr+s, ctr-sR, ctr-s, ctr+sR]; end def corners; cornerize(@center, @size, TankVector.reflect(@size)); end def front_corners; c = corners(); [c[-1],c[0]]; end def faces; cornerize(@center, [EMAIL PROTECTED],0], TankVector[0,@size[Y]]); end def screen_corners(height = 0) dz = (0 == height) ? 1 : @dz ctr = screen_coord(dz) psize = @size * dz cornerize(ctr, psize, TankVector.reflect(psize)) end # rectangular def contain? point; TankVector.within?(point - @center, @size); end def intersect? shape self.corners.any? { |point| shape.contain? point } or shape.corners.any? { |point| self.contain? point } end def angle_to shape Math.atan2(@center[Y] - shape.center[Y], @center[X] - shape.center[X]) end end class Building < Thing def initialize(west, east, north, south) center = TankVector[(east+west)/2.0, (south+north)/2.0] size = TankVector[(east-west)/2.0, (south-north)/2.0] super(center, size) end def draw_content; draw_box; end end class Boundary < Building def initialize(west, east, north, south) super @fill_color = $app.black @stroke_color = $app.silver @height = Z_SCALE/2 end end class Circle < Thing def initialize(center, radius, color) size = TankVector[radius, radius] super(center, size) @stroke_color = color @fill_color = color end def draw_content; draw_oval; end def corners; faces(); end def contain? point; (point - @center).r < @size.r; end def approach pos; @center += (pos - @center) * TIGHTNESS; end def self.bound(x, high) return -high if x < -high return high if x > high x end def self.angle_bound(t) return t + 2*Math::PI if t < -Math::PI return t - 2*Math::PI if t > Math::PI t end def angle_toward p, t angle = angle_to(p) $log.debug "Turning by angle: #{angle}" Circle.angle_bound(angle - t) end end # # Moving "Sprites" # class Sprite < Thing attr_reader :facing, :goal def turn_radius; 2.0 * @size[Y]; end def drag; [DRAG_MAX, @size[Y]/100].min; end def initialize(center, size) super @last = @center @facing = @acceleration = @speed = 0.0 @turn_max = 1.0/turn_radius @decay = 1 - drag @goal = nil @goal_color = $app.green end def set_goal pos, radius @goal = Circle.new pos, radius, @goal_color @goal.stroke_color = @stroke_color end def energy; @speed**2; end def turn return if @goal.nil? turn_by = @goal.angle_toward(self, @facing) @facing += Circle.bound(turn_by, @turn_max) @acceleration = Math.cos(turn_by) * IMPULSE * strength() end def move @speed += @acceleration @speed *= @decay @last = @center @center += TankVector[1,0].rotate_by(@facing) * @speed end def arrive @acceleration = 0 @goal = nil end def collide obstacle arrive @center = @last @speed = 0 end def at_goal?; not @goal.nil? and self.intersect?(@goal); end def update; turn(); move(); arrive if at_goal?; nil; end def draw_content; draw_oval; end end class Camera < Sprite def self.rand_within(x); rand(2*x) - x; end # Game coords go from -BOUNDS to +BOUNDS def self.random_location TankVector[rand_within(BOUNDS[X]), rand_within(BOUNDS[Y])] end def initialize tank super tank.center, SCREEN_ORIGIN @goal = tank $follow_mouse = false end def update x,y new_center = $follow_mouse ? (game_coord(x,y) + @goal.center) * 0.5 : @goal.center @center += (new_center - @center) * TIGHTNESS nil end def game_coord(x, y); TankVector[x,y] - SCREEN_ORIGIN + @center; end # Screen coords go from 0 to SCREEN_SIZE def nearby game_coord rand(SCREEN_SIZE[X]), rand(SCREEN_SIZE[Y]) end end class Explosion < Sprite def initialize shape super(shape.center, shape.size*2) @r_min = @size.r / 4.0 @fill_color = $app.orange @stroke_color = shape.stroke_color end def update @size = @size * (1-EXPLODE_RATE) nil end def dead?; @size.r < @r_min ; end end class Shell < Sprite def initialize position, angle position += MUZZLE_SIZE.rotate_by(angle) super(position, SHELL_SIZE) @facing = angle @speed = SHELL_SPEED @fill_color = @stroke_color = $app.red end def dead?; @speed < 1; end end class Turret < Sprite def reset_goal; set_goal(Vector[0,0], AIM_RADIUS); end def initialize(center, size) super @goal_color = $app.yellow reset_goal end def update_goal pos reset_goal if @goal.nil? @goal.center = pos @goal.stroke_color = @stroke_color end def alpha= level; nil; end def draw_content super muzzle = MUZZLE_SIZE.rotate_by(@facing) draw_along(muzzle) end end class TankCore < Sprite def initialize(center, color) super(center, TANK_SIZE) @turret = Turret.new(center, TANK_SIZE*0.5) @stroke_color = @turret.stroke_color = color @headlights = front_corners().inject([]) do |l,c| l << Circle.new(center, HEADLIGHT_RADIUS, color) end @will_fire = false @timer = @firing_cycle = FRAMES_PER_SECOND end def move_to pos; set_goal(pos, DEST_RADIUS); end def aim_at pos; @turret.update_goal pos; end # def move_toward pos; @goal.approach(pos) if @goal; end # def aim_toward pos; @turret.goal.approach(pos) if @turret.goal; end def health_color; :silver; end def hurt damage super @turret.fill_color = $app.send(health_color(), strength()) end def update super @turret.turn() @turret.center = @center front_corners().each_with_index {|fc, i| @headlights[i].center = fc} if @will_fire and @timer <= 0 @timer = @firing_cycle @will_fire = false return Shell.new(@center, @turret.facing) else @timer -= 1 return nil end end def corners cornerize(@center, @size.rotate_by(@facing), @size.reflect_by(@facing)) end def screen_corners cornerize(screen_coord(), @size.rotate_by(@facing), @size.reflect_by(@facing)) end def draw_content draw_outline @turret.draw @headlights.each {|b| b.draw} end end class TankPlayer < TankCore def initialize center super center, $app.blue hurt 0 end def health_color; :green; end def fire; @will_fire = true; end def draw_content super @goal.draw if not @goal.nil? @turret.goal.draw if not @turret.goal.nil? end end class TankBot < TankCore def initialize center super center, Drawable::rand_color @firing_cycle = FRAMES_PER_SECOND * N_ENEMIES aim_at $camera.nearby() hurt 0 end def health_color; :red; end def update move_to $camera.nearby() if @goal.nil? aim_at $camera.center() if @timer <= 0 move_to $camera.center() $log.debug "[EMAIL PROTECTED]: @[EMAIL PROTECTED] -> [EMAIL PROTECTED]" end @will_fire = true super end # bounce! def collide obstacle super @facing = angle_to(obstacle) move_to(@center + TankVector[BOUNCE_DISTANCE,0].rotate_by(@facing)) @speed = SHELL_SPEED end end # # Game Management # class Radar < Thing def initialize(tanks) size = SCREEN_SIZE * RADAR_SCALE offset = SCREEN_SIZE - size super offset, size @stroke_color = $app.white @fill_color = $app.black @tanks = tanks @extent = [2,2] @scale = RADAR_SCALE * SCREEN_SIZE.r / BOUNDS.r end def radar_coord pos; @center + pos * @scale; end def draw_content $app.rect *native_base_extent @tanks.each do |tank| radar_color = tank.dead? ? $app.brown : tank.stroke_color $app.stroke radar_color $app.oval *(radar_coord(tank.center).to_a + @extent) end end end class TankSpank attr :time def initialize @time = 0 @boundary = Boundary.new(-BOUNDS[X], BOUNDS[X], -BOUNDS[Y], BOUNDS[Y]) @shapes = [ [-1000, -750, -750, -250], [-500, 250, -750, -250], [500, 1000, -750, -500], [750, 1250, -250, 0], [750, 1250, 250, 750], [250, 500, 0, 750], [-250, 0, 0, 500], [-500, 0, 750, 1000], [-1000, -500, 0, 500], [400, 600, -350, -150] ].collect { |p| Building.new *p } @tank = TankPlayer.new(SCREEN_ORIGIN + TankVector[-150,-350]) @shapes << @tank $camera = Camera.new(@tank) N_ENEMIES.times do p = Camera::random_location() redo if @shapes.any? {|s| s.contain? p} @shapes << TankBot.new(p) end @radar = Radar.new @shapes.select {|s| s.is_a? TankCore} end def enemies; sprites = @shapes.select {|s| s.is_a? TankBot}; end def playing?; not @tank.dead? and enemies.size > 0; end def status s = "" s += sprintf "Time: %5.2f ", @time*1.0/FRAMES_PER_SECOND s += sprintf "⢠Strength: %.0f%% ", @tank.strength * 100 s += sprintf "⢠Enemies: %d ", enemies.size s += sprintf "â¢Â Position: %s ", TankVector.a2s(@tank.center.to_a) end def fire; @tank.fire; end def follow x,y; @tank.move_to $camera.game_coord(x,y); end def check shape if shape.dead? and not shape.is_a? Explosion @shapes.delete shape @shapes << Explosion.new(shape) $log.debug "Exploded: #{shape}" end end def update x,y $camera.update x,y @tank.aim_at $camera.game_coord(x,y) @time += 1 sprites = @shapes.select {|s| s.is_a? Sprite} visible_objects = @shapes.select {|s| s.intersect? $camera} sprites.each do |sprite| result = sprite.update @shapes << result if not result.nil? obstacles = @shapes.find_all {|s| s.intersect? sprite and s != sprite } obstacles.each do |obstacle| $log.debug "#{sprite} vs.\n\t#{obstacle}\n" sprite.encounter obstacle check sprite check obstacle end @shapes.delete sprite if sprite.dead? # in case died of natural causes end end def draw $log.debug "[EMAIL PROTECTED] @ #{$camera}" $app.clear do @boundary.draw @shapes.each { |s| s.draw if s.is_a? Thing and $camera.intersect? s } end @radar.draw end end # # Test Helpers for when NOT running inside Shoes # class AppTest def self.app(param); yield; end def self.method_missing sym, *args s = args.collect{|x|sprintf(" %.0f",x)} $log << "\t#{sym}: #{s}\n" end def self.black (*args); [1]; end def self.red (*args); [1]; end def self.green (*args); [1]; end def self.blue (*args); [1]; end def self.cyan (*args); [1]; end def self.yellow (*args); [1]; end def self.rgb (*args); args; end def self.stroke (*args); end def self.fill (*args); end def self.clear; yield; end end def stack; end; def para x; $log << x; end def banner x; $log << x; end; def caption x; $log << x; end; def mouse(); [1] + SCREEN_ORIGIN.to_a; end def keypress; yield "x"; end; def click; yield mouse(); end; def animate(n); 3.times {yield}; end # # User Interaction APP = Shoes rescue AppTest APP.app :width => SCREEN_SIZE[X], :height => SCREEN_SIZE[Y] do $app = self $app = APP if $app.class == Object @game = TankSpank.new stack do para "Game Starting...", :stroke => blue end @paused = false keypress do |key| case key when "1", "z" button, x, y = mouse() @game.follow x,y if @game.playing? when "2", "x", " " @game.fire if @game.playing? when "p" @paused = ! @paused when "m" $follow_mouse = true when "t" $follow_mouse = false when "n" @game = TankSpank.new stack do banner "New Game", :stroke => white, :margin => 10 end end end click do |button, x, y| if 1 == button @game.follow x, y # if @game.playing? else @game.fire if @game.playing? end end animate(FRAMES_PER_SECOND) do button, x, y = mouse() if not @paused @game.update x,y @game.draw end if @game.playing? stack do para @game.status, :stroke => orange, :margin => 10 para "Mouse: aim, Click: move, Space: fire, p: pause, m/t: follow mouse/tank, n: new\n", :stroke => silver, :margin => 10 para "Paused", :stroke => yellow if @paused end else stack do banner "Game Over", :stroke => white, :margin => 10 if @game.enemies.size > 0 caption "Learn to drive!", :stroke => white, :margin => 20 else caption "Congratulations!You totally r00l!", :stroke => white, :margin => 20 end end end # else end # animate end # app
Any suggestions for how to redo the drawing? Cached images? Less alpha? Fewer bullets?
-- Ernie P.
