├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── logo.gif ├── shard.lock ├── shard.yml ├── spec ├── crono_spec.cr └── spec_helper.cr └── src ├── crono.cr └── crono ├── art_supplies.cr ├── color.cr ├── font.cr ├── image.cr ├── keyboard.cr ├── math.cr ├── renderer.cr ├── song.cr ├── sound.cr ├── version.cr └── window.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /libs/ 3 | /lib/ 4 | /bin/ 5 | /.shards/ 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jeremy Woertink 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![crono](logo.gif) crono [![Build Status](https://travis-ci.com/jwoertink/crono.svg?branch=master)](https://travis-ci.com/jwoertink/crono) 2 | 3 | Crono is a 2d video game framework based around SDL2. 4 | 5 | **NOTE** Still under development, but games can be sort of played now 6 | 7 | The major part holding this back is animation is super choppy still. 8 | 9 | ## Installation 10 | 11 | Include in your `shard.yml` 12 | 13 | ```yml 14 | dependencies: 15 | crono: 16 | github: jwoertink/crono 17 | branch: master 18 | ``` 19 | 20 | Be sure to have SDL2 installed! 21 | 22 | ### macOS 23 | 24 | ``` 25 | brew install sdl2 26 | brew install sdl2_image 27 | brew install sdl2_ttf 28 | brew install sdl2_mixer --with-flac --with-fluid-synth --with-libmikmod --with-libmodplug --with-libvorbis --with-smpeg2 29 | ``` 30 | 31 | ### Linux Debian 32 | 33 | 34 | ## Usage 35 | 36 | "Hello World" example. 37 | 38 | ```crystal 39 | require "crono" 40 | 41 | class MyGameWindow < Crono::Window 42 | @main_text : Crono::Font? 43 | 44 | def after_init 45 | font_path = File.join(__DIR__, "assets", "fonts", "lobster.ttf") 46 | @main_text = Crono::Font.new(font_path, 18) 47 | @main_text.color = Crono::Color::YELLOW 48 | @main_text.text = "Hello GameWorld" 49 | end 50 | 51 | def draw 52 | brush.draw(@main_text, {100, 75}) 53 | end 54 | 55 | end 56 | 57 | new_game = MyGameWindow.new(640, 480) 58 | new_game.title = "My Super cool game" 59 | 60 | # Call this method to boot the game 61 | new_game.show 62 | ``` 63 | 64 | Check out some [Sample Games](https://github.com/jwoertink/crono-samples) 65 | 66 | ## Crono::Window 67 | 68 | This is the class that your main class will inherit from. `Crono::Window` gives you lots of important methods. 69 | 70 | * `initialize` - It's still crystal, so you can override this. Just be sure to call `initialize(width, height)` in it. 71 | * `after_init` - This method is called after SDL has had a chance to set things up. 72 | * `draw` - Define this method to draw to the screen 73 | * `update` - Define this method to update calulcations and such for the next draw to make 74 | * `key_pressed(key)` - This method is called when a key is pressed. 75 | * `key_down(key)` - This method is called when a key is down. 76 | * `key_up(key)` - This method is called when a key is released. 77 | 78 | To boot your game, you will instantiate your custom game window class that inherits from `Crono::Window`. 79 | ``` 80 | new_game = MyGameWindow.new(640, 480) 81 | ``` 82 | 83 | Then give it a title with `new_game.title = "Whatever your game is called"` 84 | Finally you call the show method `new_game.show`. 85 | 86 | 87 | ### Colors 88 | Crono has a few [built in colors](https://github.com/jwoertink/crono/blob/master/src/crono/color.cr#L4) you can use. 89 | 90 | ```crystal 91 | class MyGameWindow < Crono::Window 92 | def draw 93 | brush.draw(background_color, {0, 0}) 94 | end 95 | 96 | private def background_color 97 | Crono::Color.darken(Crono::Color::BLUE, 30) 98 | end 99 | end 100 | ``` 101 | 102 | 103 | ## Development 104 | 105 | Help me understand SDL, and write some specs. 106 | Most importantly, need to figure out how to make animation smooth. 107 | 108 | ## Contributing 109 | 110 | 1. Fork it ( https://github.com/jwoertink/crono/fork ) 111 | 2. Create your feature branch (git checkout -b my-new-feature) 112 | 3. Commit your changes (git commit -am 'Add some feature') 113 | 4. Push to the branch (git push origin my-new-feature) 114 | 5. Create a new Pull Request 115 | 116 | ## Contributors 117 | 118 | - [jwoertink](https://github.com/jwoertink) Jeremy Woertink - creator, maintainer 119 | -------------------------------------------------------------------------------- /logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwoertink/crono/c9902687f5e130560186a995ac237209428b6c9e/logo.gif -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | sdl: 4 | github: ysbaddaden/sdl.cr 5 | commit: d40de3177809ac2f71b5980632b422565eb0a3b4 6 | 7 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: crono 2 | version: 0.1.0 3 | 4 | authors: 5 | - Jeremy Woertink 6 | 7 | targets: 8 | crono: 9 | main: src/crono.cr 10 | 11 | crystal: 0.21.1 12 | 13 | license: MIT 14 | 15 | dependencies: 16 | sdl: 17 | github: ysbaddaden/sdl.cr 18 | branch: master 19 | -------------------------------------------------------------------------------- /spec/crono_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Crono do 4 | # TODO: Write tests 5 | 6 | it "works" do 7 | false.should eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/crono" 3 | -------------------------------------------------------------------------------- /src/crono.cr: -------------------------------------------------------------------------------- 1 | require "sdl" 2 | require "sdl/image" 3 | require "sdl/ttf" 4 | require "sdl/mix" 5 | require "./crono/*" 6 | 7 | 8 | SDL.init(SDL::Init::VIDEO | SDL::Init::AUDIO) 9 | SDL::TTF.init 10 | SDL::Mix.init(SDL::Mix::Init::FLAC) 11 | SDL::Mix.open 12 | 13 | # When you close, we all close 14 | at_exit { 15 | SDL::IMG.quit 16 | SDL::TTF.quit 17 | SDL::Mix.quit 18 | SDL.quit 19 | } 20 | 21 | module Crono 22 | extend GameMath 23 | @@renderer : Crono::Renderer? 24 | 25 | def self.renderer 26 | @@renderer.not_nil! 27 | end 28 | 29 | def self.renderer=(new_renderer) 30 | @@renderer = new_renderer 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/crono/art_supplies.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | module ArtSupplies 3 | def brush 4 | Crono.renderer.not_nil! 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /src/crono/color.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | struct Color 3 | # R, G, B, A 4 | WHITE = SDL::Color[255] 5 | GRAY = SDL::Color[128] 6 | BLACK = SDL::Color[0] 7 | RED = SDL::Color[255, 0, 0] 8 | ORANGE = SDL::Color[255, 171, 0] 9 | YELLOW = SDL::Color[255, 255, 0] 10 | LIME = SDL::Color[171, 255, 0] 11 | GREEN = SDL::Color[0, 255, 0] 12 | SPRING = SDL::Color[0, 255, 171] 13 | CYAN = SDL::Color[0, 255, 255] 14 | DODGER = SDL::Color[0, 171, 255] 15 | BLUE = SDL::Color[0, 0, 255] 16 | PURPLE = SDL::Color[171, 0, 171] 17 | MAGENTA = SDL::Color[255, 0, 255] 18 | PINK = SDL::Color[255, 0, 171] 19 | 20 | alias RGBA = SDL::Color 21 | alias RGB = Tuple(Int32, Int32, Int32) 22 | 23 | def self.to_hex(values : RGB) 24 | to_hex({values[0], values[1], values[2], 255}) 25 | end 26 | 27 | def self.to_hex(values : RGBA) 28 | "#%02X%02X%02X%02X" % values 29 | end 30 | 31 | # Returns `RGBA` with all non-zero values lowered by percent. 32 | # Does not change transparency 33 | def self.darken(values : RGBA, percent : Int32) 34 | numbers = [] of Int32 35 | numbers << (values[0] - (values[0] * percent) / 100) 36 | numbers << (values[1] - (values[1] * percent) / 100) 37 | numbers << (values[2] - (values[2] * percent) / 100) 38 | numbers << values[3] 39 | RGBA.from(numbers) 40 | end 41 | 42 | def self.lighten(values : RGBA, percent : Int32) 43 | numbers = [] of Int32 44 | numbers << (values[0] + (255 * percent) / 100) 45 | numbers << (values[1] + (255 * percent) / 100) 46 | numbers << (values[2] + (255 * percent) / 100) 47 | numbers << values[3] 48 | RGBA.from(numbers) 49 | end 50 | 51 | def self.rand 52 | RGB.from([rand(256), rand(256), rand(256)]) 53 | end 54 | 55 | macro [](r, g, b, a = 255) 56 | SDL::Color[{{r}}, {{g}}, {{b}}, {{a}}] 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /src/crono/font.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | class Font 3 | property text : String 4 | property color : Crono::Color::RGBA 5 | 6 | def initialize(@path : String, @font_size : Int32) 7 | @text = "" 8 | @color = Crono::Color::BLACK 9 | @sdl_font = SDL::TTF::Font.new(@path, @font_size) 10 | end 11 | 12 | def draw 13 | @sdl_font.render_solid(text, color) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/crono/image.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | class Image 3 | alias TSize = Tuple(Int32, Int32) 4 | alias Sprite = NamedTuple(image: SDL::Texture, clip: SDL::Rect) 5 | alias Tiles = Array(Sprite) 6 | @sdl_img : SDL::Texture 7 | property src, dimentions 8 | 9 | # Returns Array(NamedTuple) 10 | # The total number of sprites would be 11 | # (img.full_width / dimentions[0]) * (img.full_height / dimentions[1]) 12 | def self.load_tiles(src : String, dimentions : TSize) 13 | sprite = self.new(src, dimentions) 14 | sprites = [] of Sprite 15 | cols = (sprite.full_width / dimentions[0]).floor 16 | rows = (sprite.full_height / dimentions[1]).floor 17 | cols.times do |c| 18 | rows.times do |r| 19 | x = c * dimentions[0] 20 | y = r * dimentions[1] 21 | sprites.push({image: sprite.sdl, clip: SDL::Rect[x, y, dimentions[0], dimentions[1]]}) 22 | end 23 | end 24 | sprites 25 | end 26 | 27 | def initialize(@src : String, @dimentions : TSize) 28 | @sdl_img = init_sdl_img 29 | end 30 | 31 | def initialize(@src : String) 32 | @sdl_img = init_sdl_img 33 | @dimentions = {1,1} 34 | end 35 | 36 | # returns the width you want 37 | def width 38 | @dimentions[0]? 39 | end 40 | 41 | # returns the full width of the image 42 | def full_width 43 | sdl.width 44 | end 45 | 46 | # returns the height you want 47 | def height 48 | @dimentions[1]? 49 | end 50 | 51 | # returns the full height of the image 52 | def full_height 53 | sdl.height 54 | end 55 | 56 | # TODO: find a better name for this proxy method 57 | def sdl 58 | @sdl_img 59 | end 60 | 61 | private def init_sdl_img 62 | ext = File.extname(@src)[1..-1] 63 | #NOTE: If loading a .bmp, should it be init somehow? 64 | if ["jpg", "png", "tif"].includes?(ext) 65 | SDL::IMG.init(SDL::IMG::Init.parse(ext)) 66 | SDL::IMG.load(@src, Crono.renderer.not_nil!.sdl) 67 | else 68 | bmp = SDL.load_bmp(@src) 69 | bmp.color_key = {255, 0, 255} 70 | SDL::Texture.from(bmp, Crono.renderer.sdl) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /src/crono/keyboard.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | KbLeft = LibSDL::Keycode::LEFT 3 | KbRight = LibSDL::Keycode::RIGHT 4 | end 5 | -------------------------------------------------------------------------------- /src/crono/math.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | module GameMath 3 | 4 | def offset_x(angle : Float64, radius : Float64) 5 | Math.sin(angle / 180 * Math::PI) * radius 6 | end 7 | 8 | def offset_y(angle : Float64, radius : Float64) 9 | -Math.cos(angle / 180 * Math::PI) * radius 10 | end 11 | 12 | def distance(x1 : Float64, y1 : Float64, x2 : Float64, y2 : Float64) 13 | Math.sqrt(distance_sqr(x1, y1, x2, y2)) 14 | end 15 | 16 | def distance_sqr(x1 : Float64, y1 : Float64, x2 : Float64, y2 : Float64) 17 | square(x1 - x2) + square(y1 - y2) 18 | end 19 | 20 | def square(value : Int32 | Float64) 21 | value * value 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/crono/renderer.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | class Renderer 3 | @sdl_renderer : SDL::Renderer 4 | delegate clear, to: @sdl_renderer 5 | property animation_counter : UInt16 6 | 7 | def initialize(window : SDL::Window) 8 | @sdl_renderer = SDL::Renderer.new(window) 9 | @animation_counter = 0_u16 10 | end 11 | 12 | def sdl 13 | @sdl_renderer 14 | end 15 | 16 | def color=(color) 17 | sdl.draw_color = color 18 | sdl.clear 19 | color 20 | end 21 | 22 | def color 23 | sdl.draw_color 24 | end 25 | 26 | # Draws an image 27 | def draw(image : Image, location : Tuple(Int32, Int32), angle = 0) 28 | #sdl.viewport = {location[0], location[1], image.width, image.height} 29 | sdl.copy(image.sdl, nil, SDL::Rect[location[0], location[1], image.width, image.height], angle) 30 | end 31 | 32 | # Draws some text 33 | def draw(font : Font, location : Tuple(Int32, Int32)) 34 | surface = font.draw 35 | sdl.copy(surface, dstrect: SDL::Rect[location[0], location[1], surface.width, surface.height]) 36 | end 37 | 38 | # Draws an image for the current animation 39 | def animate(image_sprites, location : Tuple(Int32, Int32)) 40 | count = image_sprites.size 41 | @animation_counter = 0_u16 if @animation_counter >= count 42 | sprite = image_sprites[@animation_counter] 43 | image = sprite[:image] 44 | sdl.copy(image, sprite[:clip], SDL::Rect[location[0], location[1], sprite[:clip].w, sprite[:clip].h]) 45 | @animation_counter += 1 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/crono/song.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | class Song 3 | @sdl : SDL::Mix::Music 4 | delegate playing?, paused?, to: @sdl 5 | 6 | def initialize(@src : String) 7 | @sdl = SDL::Mix::Music.new(@src) 8 | end 9 | 10 | def play 11 | return if playing? 12 | @sdl.play 13 | end 14 | 15 | def pause 16 | return if paused? 17 | @sdl.pause 18 | end 19 | 20 | def resume 21 | return if playing? 22 | @sdl.resume 23 | end 24 | 25 | # Toggle music between playing and pausing 26 | def toggle 27 | if paused? 28 | resume 29 | elsif playing? 30 | pause 31 | else 32 | play 33 | end 34 | end 35 | 36 | def stop 37 | resume if paused? 38 | @sdl.stop 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/crono/sound.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | class Sound 3 | 4 | def initialize(@src : String, channel = 0) 5 | @sdl = SDL::Mix::Sample.new(@src) 6 | @channel = SDL::Mix::Channel.new(channel) 7 | end 8 | 9 | def play 10 | @channel.play(@sdl) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/crono/version.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /src/crono/window.cr: -------------------------------------------------------------------------------- 1 | module Crono 2 | abstract class Window 3 | include ArtSupplies 4 | property width : Int32 5 | property height : Int32 6 | property title : String 7 | 8 | @sdl_window : SDL::Window? 9 | 10 | def initialize(width, height) 11 | @width = width 12 | @height = height 13 | @title = "Sample" 14 | @close_window = false 15 | end 16 | 17 | def show 18 | init_sdl! 19 | after_init 20 | run_game_loop 21 | end 22 | 23 | def after_init 24 | end 25 | 26 | def clear_screen 27 | Crono.renderer.clear 28 | end 29 | 30 | def close 31 | @close_window = true 32 | end 33 | 34 | abstract def update 35 | abstract def draw 36 | 37 | def key_pressed(key) 38 | end 39 | 40 | def key_down(key) 41 | end 42 | 43 | def key_up(key) 44 | end 45 | 46 | def sdl 47 | @sdl_window.not_nil! 48 | end 49 | 50 | private def init_sdl! 51 | @sdl_window = SDL::Window.new(title, width, height) 52 | Crono.renderer = Crono::Renderer.new(sdl) 53 | end 54 | 55 | private def current_time 56 | Time.now.epoch_ms 57 | end 58 | 59 | private def run_game_loop 60 | update_interval = 120 61 | draw_interval = 60 62 | time_of_last_update = current_time 63 | time_of_last_draw = current_time 64 | loop do 65 | case event = SDL::Event.poll 66 | when SDL::Event::Quit 67 | break 68 | when SDL::Event::Keyboard 69 | key_pressed(event.sym) if event.pressed? 70 | key_down(event.sym) if event.keydown? 71 | key_up(event.sym) if event.keyup? 72 | end 73 | 74 | time_until_update = (update_interval + time_of_last_update) - current_time 75 | time_until_draw = (draw_interval + time_of_last_draw) - current_time 76 | work_done = false 77 | 78 | if time_until_update <= 0 79 | update 80 | #sdl.update # Do I need this? 81 | Crono.renderer.sdl.present 82 | time_of_last_update = current_time 83 | work_done = true 84 | end 85 | 86 | if time_until_draw <= 0 87 | clear_screen 88 | draw 89 | time_of_last_draw = current_time 90 | work_done = true 91 | end 92 | 93 | if work_done == false 94 | sleep_seconds = time_until_update 95 | 96 | if time_until_draw < sleep_seconds 97 | sleep_seconds = time_until_draw 98 | end 99 | time_to_sleep = sleep_seconds * 0.001 100 | sleep(time_to_sleep) 101 | end 102 | 103 | break if @close_window 104 | end 105 | end 106 | 107 | end 108 | end 109 | --------------------------------------------------------------------------------