= CHINGU
http://github.com/ippa/chingu/tree/master
DOCUMENTATION: http://rdoc.info/projects/ippa/chingu
Ruby 1.9.1 is recommended. Also works with 1.8.7+.
This is still a rather young project, core classes and naming can still change.
== INSTALL
gem sources -a http://gems.github.com
sudo gem install ippa-chingu
== DESCRIPTION
Game framework built on top of the OpenGL accelerated game lib Gosu.
It adds simple yet powerful game states, prettier input handling, deployment safe asset-handling, a basic re-usable game object and automation of common task.
== THE STORY
The last years I've dabbled around a lot with game development.
I've developed games in both Rubygame and Gosu. I've looked at gamebox.
Rubygame is a very capable framework with a lot of functionality (collision detection, very good event system etc). Gosu is way more minimalistic but also faster with OpenGL -acceleration. Gosu isn't likely to get much more complex since it does what it should do very well and fast.
After 10+ game prototypes and some finished smaller games I started to see patterns each time I started a new game. Making classes with x/y/image/other-parameters that I called update/draw on in the main loop.
This became the basic Chingu::GameObject which encapsulates Gosus "Image.draw_rot" and enables automatic updating/drawing through "game_objects".
There was always a huge big chunk of checking keyboard-events in the main loop.
Borrowing ideas from Rubygame this has now become @player.keyboard(:left => :move_left, :space => :fire ... etc.
== CORE OVERVIEW
Chingu consists of the following core classes / concepts:
=== Chingu::Window
The main window, use it at you use Gosu::Window now. Calcs the framerate, takes care of states,
handles chingu-formated input, updates and draws BasicGameObject / GameObjects automaticly.
Available thoughout your source as $window (Yes, that's the only global Chingu has).
=== Chingu::GameObject
Use this for all your in game objects. The player, the enemies, the bullets, the powerups, the loot laying around.
It's very reusable and doesn't contain any game-logic (that's up to you!). Only stuff to put it on screen a certain way.
If you do GameObject.create() instead of new() Chingu will keep save the object in the "game_object"-list for automatic updates/draws.
GameObjects also have the nicer Chingu input-mapping: @player.input = { :left => :move_left, :right => :move_right, :space => :fire}
Has either Chingu::Window or a Chingu::GameState as "parent".
=== Chingu::BasicGameObject
For those who think GameObject is a too little fat, there's BasicGameObject (GameObject inherits from BasicGameObject).
BasicGameObject is just an empty frame (no x,y,image accessors or draw-logic) for you to build on.
It can be extended with Chingus trait-system though. The new() vs create() behaivor of GameObject comes from BasicGameObject.
BasicGameObject#parent points to either $window or a game state and is automaticly set on creation time.
=== Chingu::GameStateManager
Keeps track of the game states. Implements a stack-based system with push_game_state and pop_game_state.
=== Chingu::GameState
A "standalone game loop" that can be activated and deactivated to control game flow.
A game state is very much like a main gosu window. You define update() and draw() in a gamestate.
It comes with 2 extras that main window doesn't have. #setup (called when activated) and #finalize (called when deactivated)
If using game states, the flow of draw/update/button_up/button_down is:
Chingu::Window --> Chingu::GameStateManager --> Chingu::GameState.
For example, inside game state Menu you call push_game_state(Level). When Level exists, it will go back to Menu.
=== Traits
Traits are extensions (or plugins if you so will) to BasicGameObjects.
The aim is so encapsulate common behaivor into modules for easy inclusion in your game classes.
Making a trait is easy, just an ordinary module with the methods setup_trait(), update_trait() and/or draw_trait().
It currently has to be namespaced to Chingu::Traits for "has_trait" to work inside GameObject-classes.
== OTHER CLASSES / HELPERS
=== Chingu::Text
Makes use of Gosu::Font more rubyish and powerful.
In it's core, another Chingu::GameObject + Gosu::Font.
=== Chingu::Animation
Load and interact with tile-based animations. loop, bounce and access invidual frame(s) easily.
An "@image = @animation.next!" in your Player#update is usually enough to get you started!
=== Chingu::Parallax
A class for easy paralaxxscrolling. See example3.rb for more.
=== Various Helpers
Both $window and game states gets some new graphical helpers, currently only 3, but quite useful:
fill() # Fills whole window with color 'color'.
fill_rect() # Fills a given Rect 'rect' with Color 'color'
fill_gradient() # Fills window or a given rect with a gradient between two colors.
If you base your models on GameObject (or BasicGameObject) you get:
Enemy.all # Returns all object based on this class
Enemy.each # Iterates through all objects of class Enemt.
Enemy.destroy_if(&block) # destroy all objects for which &block returns true
== BASICS / EXAMPLES
=== Chingu::Window
With Gosu the main window inherits from Gosu::Window. In Chingu we use Chingu::Window. It's a basic Gosu::Window with extra cheese on top of it. keyboard handling, automatic update/draw calls to all gameobjects, fps counting etc.
You're probably familiar with this very common Gosu pattern:
ROOT_PATH = File.dirname(File.expand_path(FILE))
class Game < Gosu::Window
def initialize
@player = Player.new
end
def update
if button_down? Button::KbLeft
@player.left
elsif button_down? Button::KbRight
@player.right
end
@player.update
end
def draw
@player.draw
end
end
class Player
attr_accessor :x,:y,:image
def initialize(options)
@x = options[:x]
@y = options[:y]
@image = Image.new(File.join(ROOT_PATH, "media", "player.png"))
end
def move_left
@x -= 1
end
def move_right
@x += 1
end
def draw
@image.draw(@x,@y,100)
end
end
Game.new.show # Start the Game update/draw loop!
Chingu doesn't change any fundamental concept of Gosu, but it will make the above code cleaner:
We use Chingu::Window instead of Gosu::Window
class Game < Chingu::Window
def initialize
super # This is always needed if you want to take advantage of what chingu offers
#
# Player will automaticly be updated and drawn since it's a Chingu::GameObject
# You'll need your own Game#update/#draw after a while, but just put #super there and Chingu can do its thing.
#
@player = Player.create
@player.input = {:left => :move_left, :right => :move_right}
end
end
If we create classes from Chingu::GameObject we get stuff for free.
The accessors: image,x,y,zorder,angle,factor_x,factor_y,center_x,center_y,mode,update,draw
And in it's core, that's what Chingu::GameObject is, an encapsulation of draw_rot with some extra cheese.
For example, we get automatic calls to draw/update with Chingu::GameObject, which usually is what you want.
You could stop this by doing: @player = Player.new(:draw => false, :update => false)
class Player < Chingu::GameObject
def initialize(options)
super(options.merge(:image => Image["player.png"])
end
def move_left
@x -= 1
end
def move_right
@x += 1
end
end
Game.new.show # Start the Game update/draw loop!
Roughly 50 lines became 26 more powerful lines. (you can do @player.angle = 100 for example)
If you've worked with Gosu for a while you're probably tired of passing around the window-parameter.
Chingu solves this (as has many other developers) with a global variable $window. Yes, globals are bad, but in this case it kinda makes sense. It's used under the hood in various places.
=== Chingu::GameObject
This is our basic "game unit"-class, meaning most in game objects (players, enemies, bullets etc) should be inherited from Chingu::GameObject.
The basic ideas behind it are:
- Encapsulate only the very common basics that Most in game objects need
- Keep naming close to Gosu, but add smart convenient methods / shortcuts and a more rubyish feeling
- No game logic allowed in GameObject, since that's not likely to be useful for others.
I've chose to base it around Image#draw_rot. So basically all the arguments that you pass to draw_rot can be passed to GameObject#new when creating a new object, an example using almost all arguments would be:
@player = Player.new(:image => Image["player.png"], :x=>100, :y=>100, :zorder=>100, :angle=>45, :factor_x=>10, :factor_y=>10, :center_x=>0, :center_y=>0)
A shortcut for the above line would be
@player = Player.new(:image => Image["player.png"], :x=>100, :y=>100, :zorder=>100, :angle=>45, :factor=>10, :center=>0)
I've tried doing sensible defaults:
x/y = [middle of the screen] for super quick display where it should be easy in sight)
angle = 0 (no angle by default)
center_x/center_y = 0.5 (basically the center of the image will be drawn at x/y)
factor_x/factor_y = 1 (no zoom by default)
@player = Player.new
By default Chingu::Window calls update & draw on all GameObjects in it's own update/draw.
If this is not what you want, use :draw and :update
@player = Player.new(:draw => false, :update => false)
=== Input
One of the core things I wanted a more natural way of inputhandling.
You can define input -> actions on Chingu::Window, Chingu::GameState and Chingu::GameObject.
Like this:
When left arrow is pressed, call @player.turn_left ... and so on.
@player.input = { :left => :turn_left, :right => :turn_right, :left => :halt_left, :right => :halt_right }
In Gosu the equivalent would be:
def button_down(id)
@player.turn_left if id == Button::KbLeft
@player.turn_right if id == Button::KbRight
end
def button_up(id)
@player.halt_left if id == Button::KbLeft
@player.halt_right if id == Button::KbRight
end
Another more complex example:
So what happens here?
Pressing P would create an game state out of class Pause, cache it and activate it.
Pressing ESC would call Play#close
Holding down LEFT would call Play#move_left on every game iteration
Holding down RIGHT would call Play#move_right on every game iteration
Releasing SPACE would call Play#fire
class Play < Chingu::GameState
def initialize
self.input = { :p => Pause,
:escape => :close,
:holding_left => :move_left,
:holding_right => :move_right,
:released_space => :fire }
end
end
class Pause < Chingu::GameState
# pause logic here
end
In Gosu the above code would include code in button_up(), button_down() and a check for button_down?() in update().
Every symbol can be prefixed by either "released_" or "holding_" while no prefix at all defaults to pressed once.
So, why not :up_space or :release_space instead of :released_space?
+:up_space+ doesn't sound like english, :release_space sounds more like a command then an event.
Or +:hold_left+ or :down_left instead of :holding_left?
:holding_left sounds like something that's happening over a period of time, not a single trigger, which corresponds well to how it works.
And with the default :space => :something you would imagine that :something is called once. You press :space once, :something is executed once.
=== GameState / GameStateManager
Chingu incorporates a basic push/pop game state system (as discussed here: http://www.gamedev.net/community/forums/topic.asp?topic_id=477320).
Game states is a way of organizing your intros, menus, levels.
Game states aren't complicated. In Chingu a GameState is a class that behaves mostly like your default Gosu::Window (or in our case Chingu::Window) game loop.
A simple GameState-example
class Intro < Chingu::GameState
def update
# game logic here
end
def draw
# screen manipulation here
end
# Called when we enter the game state
def setup
@player.angle = 0 # point player upwards
end
# Called when we leave the game state
def finalize
push_game_state(Menu) # switch to game state "Menu"
end
end
Looks familiar yet?
You can activate the above game state in 2 ways
class Game < Chingu::Window
def initialize
#
# 1) Create a new Intro-object and activate it (pushing to the top).
# This version makes more sense if you want to pass parameters to the gamestate, for example:
# push_game_state(Level.new(:level_nr => 10))
#
push_game_state(Intro.new)
#
# 2) This leaves the actual object-creation to the game state manager.
# Intro#initialize() is called, then Intro#setup()
#
push_game_state(Intro)
end
end
Another example:
class Game < Chingu::Window
def initialize
#
# We start by pushing Menu to the game state stack, making it active as the only state on stack.
# :setup => :false which will skip setup() from being called (standard when switching to a new state)
#
push_game_state(Menu, :setup => false)
#
# We push another game state to the stack, Play. We now have 2 states, which active being first / active.
#
# :finalize => false will skip setup() from being called on game state
# that's being pushed down the stack, in this case Intro.setup().
#
push_game_state(Play, :finalize => false)
#
# This would remove Play state from the stack, going back to the Menu-state. But also:
# .. skipping the standard call to Menu#setup (the new game state)
# .. skipping the standard call to Play#finalize (the current game state)
#
# :setup => false can for example be useful when pop'ing a Pause game state. (see example4.rb)
#
pop_game_state(:setup => false, :finalize => :false)
#
# Replace the current game state with a new one.
# :setup and :finalize options are available here as well.
#
switch_game_state(Credits)
end
end
A GameState in Chingu is just a class with the following instance methods:
- initialize() - called only once with push_game_state(Intro) but every time with push_game_state(Intro.new)
- setup() - called each time the game state becomes active.
- button_down(id) - Called when a button is down
- button_up(id) - Called when a button is released
- update() - just as in your normal game loop, put your game logic here.
- draw() - just as in your normal game loop, put your screen manipulation here.
- finalize() - called when a game state de-activated (for example by pushing a new one on top with push_game_state)
Chingu::Window automatically creates a @game_state_manager and makes it accessible in our game loop.
By default the game loop calls update() / draw() on the the current game state.
Chingu also has a couple of helpers-methods for handling the game states:
In a main loop or in a game state:
- push_game_state(state) - adds a new gamestate on top of the stack, which then becomes the active one
- pop_game_state - removes active gamestate and activates the previous one
- switch_game_state(state) - replaces current game state with a new one
- current_game_state - returns the current game state
- previous_game_state - returns the previous game state (useful for pausing and dialog boxes, see example4.rb)
- pop_until_game_state(state) - pop game states until given state is found
- clear_game_states - removes all game states from stack
To switch to a certain gamestate with a keypress use Chingus input handler:
class Intro < Chingu::GameState
def setup
self.input = { :space => lambda{push_gamestate(Menu.new)} }
end
end
Or Chingus shortcut:
class Intro < Chingu::GameState
def setup
self.input = { :space => Menu }
end
end
Chingus inputhandler will detect that Menu is a gamestate-class, create a new instance, cache it and activate it with push_game_state().
=== Assets / Paths
You might wonder why this is necessary in the straight Gosu example:
ROOT_PATH = File.dirname(File.expand_path(FILE))
@image = Image.new(File.join(ROOT_PATH, "media", "player.png"))
It enables you to start your game from any directory and it will still find your assets (pictures, samples, fonts etc..) correctly.
For a local development version this might not be important, you're likely to start the game from the games root-dir.
But as soon as you try to deploy (for example to windows with OCRA - http://github.com/larsch/ocra/tree/master) you'll run into trouble of you don't do it like that.
Chingu solves this problem behind the scenes for the most common assets. The 2 lines above can be replaced with:
Image["player.png"]
You also have:
Sound["shot.png"]
Song["intromusic.ogg"]
By default Image, Sound and Sound searches the current directory and directory "media".
Add your own searchpaths like this:
Gosu::Image.autoload_dirs << File.join($window.root, "gfx")
Gosu::Sound.autoload_dirs << File.join($window.root, "samples")
This will add \path\to\your\game\gfx and \path\to\your\game\samples to Image and Sound.
Thanks to Jacious of rubygame-fame (http://rubygame.org/) for his named resource code powering this.
Tiles and fonts are trickier since they require extra parameters so you'll have to do those the ordinary way.
You have $window.root (equivalent to ROOT_PATH above) for free though which points to the dir containing the game.
=== Text
Text is a class to give the use of Gosu::Font more rubyish feel and fit it better into Chingu.
Pure Gosu
@font = Gosu::Font.new($window, "verdana", 30)
@font.draw("A Text", 200, 50, 55, 2.0)
Chingu
@text = Chingu::Text.create(:text => "A Text", :x => 200, :y => 50, :zorder => 55, :factor_x => 2.0)
@text.draw
@text.draw is usually not needed as Text is a GameObject and therefore automaticly updated/drawn (it #create is used instead of #new)
It's not only that the second example is readable by ppl now even familiar with Gosu, @text comes with a number of changeable properties, x,y,zorder,angle,factor_x,color,mode etc.
Set a new x or angle or color and it will instantly update on screen.
== MISC / FAQ
=== How do I access my main-window easily?
Chingu keeps a global variable, $window, which contains the Chingu::Window instance.
Since Chingu::Window is just Gosu::Window + some cheese you can do your $window.button_down?, $window.draw_line() etc from anywhere.
See http://www.libgosu.org/rdoc/classes/Gosu/Window.html for a full set of methods.
=== How did you decide on naming of methods / classes?
There's 1 zillion ways of naming stuff. As a general guideline I've tried to follow Gosus naming.
If Gosu didn't have a good name for a certain thing/method I've checked Ruby itself and then Rails since alot
of Ruby-devs are familiar with Rails.
== TODO:
- add :padding and :align => :topleft etc to class Text
- (skip) rename Chingu::Window so 'include Chingu' and 'include Gosu' wont make Window collide
- (done) BasicObject vs GameObject vs ScreenObject => Became BasicGameObject and GameObject
- (50%) some kind of componentsystem for GameObject (which should be cleaned up)
- (done) scale <--> growth parameter. See trait "effect"
- (done) Enemy.all ... instead of game_objects_of_type(Enemy) ? could this be cool / understandable?
- (done) Don't call .update(time) with timeparameter, make time available thru other means when needed.
- (10% done) debug screen / game state.. check out shawn24's elite irb sollution :)
- (done) Complete the input-definitions with all possible inputs (keyboard, gamepad, mouse)!
- (done) Complete input-stuff with released-states etc
- (done) More gfx effects, for example: fade in/out to a specific color (black makes sense between levels).
- (posted request on forums) Summon good proven community gosu snippets into Chingu
- (done) Generate docs @ ippa.github.com- http://rdoc.info/projects/ippa/chingu !
- (done) A good scene-manager to manage welcome screens, levels and game flow- GameStateManager / GameState !
- (20% done) make a playable simple game in examples\ that really depends on game states
- (done) Make a gem- first gem made on github
- (done) Automate gemgenning rake-task even more
- (done) More examples when effects are more complete
- class ChipmunkObject
- (done) class Actor/MovingActor with maybe a bit more logic then the basic GameObject.
- (60% done) Spell check all docs, sloppy spelling turns ppl off. tnx jduff ;).
- Tests
- (done) Streamline fps / tick code
- (done) Encapsulate Font.new / draw_rot with a "class Text < GameObject"
- (10% done) Make it possible for ppl to use the parts of Chingu they like
- (done) At least make GameStateManager really easy to use with pure Gosu / Document it!
- (50% done) Get better at styling rdocs
- (done) all �gamestate� ? �game state� ? it's "game state"
- (skipping) intergrate MovieMaker - solve this with traits instead.
- A more robust game state <-> game_object system to connect them together.
- FIX example4: :p => Pause.new would Change the "inside_game_state" to Pause and make @player belong to Pause.
== WHY?
- Plain Gosu is very minimalistic, perfect to build some higher level logic on!
- Deployment and asset handling should be simple
- Managing game states/scenes (intros, menus, levels etc) should be simple
- There are patterns in game development
== OPINIONS
- Less code is usually better
- Hash arguments FTW. And it becomes even better in 1.9.
- Don't separate too much from Gosus core-naming
== CREDITS:
- Jacius of Rubygame (For doing cool stuff that's well documented as re-usable). So far rect.rb and named_resource.rb is taken from Rubygame.
- Jduff for input / commits.
- Jlnr,Philymore,Shawn24,JamesKilton for constructive feedback/discussions.
- Thanks to http://github.com/tarcieri for require_all code, good stuff
== REQUIREMENTS:
- Gosu latest version
- Ruby 1.8.7+ (Works with 1.9+ as well!)