#!/usr/local/bin/ruby # Copyright 2007 Jay McGavren, jay@mcgavren.com. # # This file is part of Zyps. # # Zyps is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser 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 Lesser General Public License # along with this program. If not, see . begin require 'zyps' require 'zyps/actions' require 'zyps/conditions' require 'zyps/views/trails' rescue LoadError require 'rubygems' require 'zyps' require 'zyps/actions' require 'zyps/conditions' require 'zyps/views/trails' end class Demo #The view width. WIDTH = 400 #The view height. HEIGHT = 300 #Number of frames to draw per demo. FRAME_COUNT = 80 #Number of frames per second. FRAMES_PER_SECOND = 30 #Default size of game objects. DEFAULT_OBJECT_SIZE = 78.5 #5 units in radius. #Set up a window, a canvas, and an object environment, then run the given block. def demo #Create a window, and set GTK up to quit when it is closed. window = Gtk::Window.new window.signal_connect("delete_event") {false} window.signal_connect("destroy") {Gtk.main_quit} #Add view to window. @view = TrailsView.new(WIDTH, HEIGHT) window.add(@view.canvas) window.show_all #Create environment. @environment = Environment.new #Point view at environment. @environment.add_observer(@view) #Run the given block. yield #Activate the GUI. Gtk.main #A divider for explanation text between demos. say("-" * 30) end #Animate an environment for a given number of frames. def animate(frame_count) #A clock to track frames to draw this second. clock = Clock.new time_per_frame = 1.0 / FRAMES_PER_SECOND begin (1..frame_count).each do |i| @environment.interact #Determine how much time is left in this frame. time_left_in_frame = (time_per_frame) - clock.elapsed_time #Sleep for the remaining time. if time_left_in_frame > 0 sleep time_left_in_frame #Skip a frame if things are going too slow. else sleep time_per_frame end end rescue Exception => exception puts exception, exception.backtrace end end #Populate an environment with the given number of creatures. def populate(environment, count = 50) count.times do |i| multiplier = i / count.to_f environment.objects << Creature.new( i, #Name. Location.new(multiplier * @view.width, multiplier * @view.height), Color.new(multiplier, 1 - multiplier, multiplier / 2 + 0.5), Vector.new(100 * multiplier, multiplier * 360), 0, #Age. DEFAULT_OBJECT_SIZE #Size. ) end end #Explain what's going on to the user. def say(phrase) puts phrase end #Demonstrates drawing an environment and changing its size. def test_render thread = Thread.new do say("The things that populate an environment are called GameObjects. Each object has:") object = GameObject.new say("...a name") object.name = "Huey" say("...a size") object.size = DEFAULT_OBJECT_SIZE say("...a Location with x and y coordiates") object.location = Location.new(@view.width/2, @view.height/2) say("...a Color with red, green and blue components ranging from 0 to 1") object.color = Color.new(1, 0, 0) say("...and a Vector giving its speed and an angle from 0 to 360.") object.vector = Vector.new(10, 45) say("Once your object is ready, add it to the environment.") @environment.objects << object say("Add a view as an observer of an Environment, and it will draw the objects.") say("Call an environment's interact() method to have the objects in it move around.") say("Our demo's animate() method does this for us.") animate(FRAME_COUNT) say("Let's add a couple more objects with different colors and vectors.") @environment.objects << GameObject.new( "Duey", #Name. Location.new(@view.width/2, @view.height/2), Color.new(0, 1, 0), Vector.new(20, 135), 0, #Age. DEFAULT_OBJECT_SIZE * 2 #Size. ) @environment.objects << GameObject.new( "Louie", #Name. Location.new(@view.width/2, @view.height/2), Color.new(0, 0, 1), Vector.new(30, 225), 0, #Age. DEFAULT_OBJECT_SIZE * 3 #Size. ) animate(FRAME_COUNT) say("The viewing area can be resized at any time via its width and height attributes.") @view.width += 100 @view.height += 100 animate(FRAME_COUNT) say("TrailsView lets you set the length of the trails as well.") @view.trail_length = 50 animate(FRAME_COUNT) end end class Gravity < EnvironmentalFactor #Accelerate object toward the 'ground'. def act(target) target.vector.y += 9.8 end end #Demonstrates environmental factors by adding gravity to the environment. def test_environmental_factors populate(@environment) thread = Thread.new do say("Without gravity, objects just travel on forever.") animate(FRAME_COUNT) say("Let's add a new EnvironmentalFactor to simulate gravity.") gravity = Gravity.new say("We add gravity to the Environment.") @environment.environmental_factors << gravity say("Everything immediately drops.") animate(FRAME_COUNT) end end #Demonstrates creature behaviors. def test_behaviors populate(@environment) thread = Thread.new do say("Let's add a Behavior to our creatures.") chase = Behavior.new say("A Behavior has one or more Action objects that define an action to take on the current target.") say("We'll add an Action that makes the creatures head straight toward their target.") chase.actions << FaceAction.new say("A Behavior also has one or more Condition objects.") say("Unless every Condition is true, the action(s) won't be carried out.") say("So that they don't target every creature on the screen, we'll add a condition to the behavior saying the target must have the label 'food'.") chase.conditions << TagCondition.new("food") say("We'll apply this behavior to all creatures currently in the environment.") @environment.objects.each {|creature| creature.behaviors << chase} animate(FRAME_COUNT) say("Then we'll toss a piece of food (a GameObject with the label 'food') into the environment.") @environment.objects << GameObject.new( "target", #Name. Location.new(@view.width / 2, @view.height / 2), Color.new(1, 1, 1), Vector.new(50, 315), 0, #Age. DEFAULT_OBJECT_SIZE * 2, #Size. ["food"] #Tags. ) say("Let's see what the creatures do.") animate(FRAME_COUNT) end end #A Creature that changes the colors of other objects. class Morpher < Creature #Changes an object's color. def initialize(*arguments) super morph = Behavior.new #Shift the target's color to match the creature's. morph.actions << BlendAction.new(self.color) #Act only on nearby targets. morph.conditions << ProximityCondition.new(50) @behaviors << morph end end #Demonstrates changing object colors. def test_change_color populate(@environment) say("Creatures can influence any attribute of their target, such as its color.") say("This demo includes a Morpher class, which is a type of Creature.") say("Morphers are created with a single behavior, which shifts the color of any nearby target to match the Morpher's color.") say("Let's place a red Morpher...") @environment.objects << Morpher.new(nil, Location.new(0, 100), Color.new(1, 0, 0), Vector.new(100, 0), 0, DEFAULT_OBJECT_SIZE) say("a green one...") @environment.objects << Morpher.new(nil, Location.new(0, 150), Color.new(0, 1, 0), Vector.new(200, 0), 0, DEFAULT_OBJECT_SIZE) say("and a blue one...") @environment.objects << Morpher.new(nil, Location.new(0, 200), Color.new(0, 0, 1), Vector.new(300, 0), 0, DEFAULT_OBJECT_SIZE) say("And see what they do.") thread = Thread.new {animate(FRAME_COUNT)} end #Demonstrates altering object speed. def test_accelerate populate(@environment) thread = Thread.new do say("Here are some Creatures, just plodding along.") animate(FRAME_COUNT / 4) say("We're going to have them pick up the pace.") say("We add a Behavior with an AccelerateAction to all the creatures, and specify they should increase their speed by 100 units/second...") @environment.objects.each do |creature| accelerate = Behavior.new accelerate.actions << AccelerateAction.new(100) creature.behaviors << accelerate end say("And watch them rocket away.") animate(FRAME_COUNT) end end #Demonstrates altering object vectors. def test_turn populate(@environment, 20) thread = Thread.new do say("This time we'll use the TurnAction class.") animate(FRAME_COUNT / 2) say("We tell each creature it should turn by 90 degrees/second...") @environment.objects.each do |creature| turn = Behavior.new turn.actions << TurnAction.new(90) creature.behaviors << turn end say("And watch things spiral out of control.") animate(FRAME_COUNT) end end #Demonstrates adding vectors. def test_approach populate(@environment, 50) say("When your car skids on ice, you might steer in a different direction, but you're going to keep following your original vector for a while.") say("Our ApproachAction adds the vector the creature WANTS to follow to the vector it's ACTUALLY following.") say("We add a behavior with an ApproachAction to all creatures...") say("We also add a condition that it should only target food.") @environment.objects.each do |creature| approach = Behavior.new approach.actions << ApproachAction.new(creature.vector) approach.conditions << TagCondition.new("food") creature.behaviors << approach end say("Add a target...") @environment.objects << Creature.new( "target", Location.new(@view.width / 2, @view.height / 3), Color.new(1, 1, 1), Vector.new(3, 0), 0, #Age. DEFAULT_OBJECT_SIZE, #Size. ["food"] #Tags. ) say("And watch them all TRY to catch it.") thread = Thread.new {animate(FRAME_COUNT)} end #Demonstrates adding vectors. def test_flee populate(@environment, 50) say("A FleeAction is just like an ApproachAction, but we head in the OPPOSITE direction.") @environment.objects.each do |creature| flee = Behavior.new flee.actions << FleeAction.new(creature.vector) flee.conditions << TagCondition.new("predator") creature.behaviors << flee end @environment.objects << Creature.new( "target", Location.new(@view.width / 2, @view.height / 2), Color.new(1, 1, 1), Vector.new(3, 0), 0, #Age. DEFAULT_OBJECT_SIZE, #Size. ["predator"] #Tags. ) thread = Thread.new {animate(FRAME_COUNT)} end #Demonstrates keeping a reference to an Environment so a Creature can alter it. def test_eat populate(@environment) say("Most games are all about destruction, but there hasn't been much so far.") say("Let's create a creature that causes some havoc.") predator = Creature.new(nil, Location.new(0, 150), Color.new(0, 1, 0), Vector.new(200, 0), 0, DEFAULT_OBJECT_SIZE * 2) say("The EatAction eats targets by removing them from their environment.") say("Creatures and their Actions normally know nothing about the Environment they belong to, so EatAction takes an Environment in its constructor.") say("EatAction finds the target in Environment.objects and removes it.") action = EatAction.new(@environment) say("Create a behavior...") behavior = Behavior.new say("Add the action to the behavior...") behavior.actions << action say("Add a condition that they must collide first...") behavior.conditions << CollisionCondition.new say("Add the behavior to the creature...") predator.behaviors << behavior say("Drop the creature into the actual environment...") @environment.objects << predator say("And - chomp!") thread = Thread.new {animate(FRAME_COUNT)} end #Run all the demos. def main say "This is a demonstration of the Zyps library." say "After each demo, close the window to proceed." say("-" * 30) demo {test_render} demo {test_environmental_factors} demo {test_behaviors} demo {test_change_color} demo {test_accelerate} demo {test_turn} demo {test_approach} demo {test_flee} demo {test_eat} say "To learn more about how the library works, you can read the source code in the 'bin/zyps_demo' file in the Zyps distribution." say "And if you want to code your own Actions, Conditions, EnvironmentalFactors, or Views, see the distribution's 'lib' folder for examples." say "Thanks for watching!" end end begin #Run the demos. Demo.new.main rescue => exception #Print error to STDERR and exit with an abnormal status. abort "Error: " + exception.message end