From 6d450f18716e59b98ba5305a0667e96d13d62959 Mon Sep 17 00:00:00 2001 From: Alex Clink Date: Wed, 10 Nov 2021 20:37:45 -0500 Subject: [PATCH] Initial Commit --- .editorconfig | 9 ++++ .gitignore | 6 +++ Info.plist | 26 ++++++++++ LICENSE | 21 ++++++++ README.md | 27 ++++++++++ lib-sdl.yaml | 10 ++++ package.rb | 84 +++++++++++++++++++++++++++++++ shard.lock | 10 ++++ shard.yml | 20 ++++++++ spec/asteroids_spec.cr | 9 ++++ spec/spec_helper.cr | 2 + src/asteroids.cr | 69 +++++++++++++++++++++++++ src/bullet.cr | 13 +++++ src/lx_game/controller.cr | 31 ++++++++++++ src/lx_game/game.cr | 68 +++++++++++++++++++++++++ src/lx_game/graphics.cr | 39 ++++++++++++++ src/lx_game/sprite.cr | 23 +++++++++ src/lx_game/vector_sprite.cr | 25 +++++++++ src/ship.cr | 98 ++++++++++++++++++++++++++++++++++++ 19 files changed, 590 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Info.plist create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib-sdl.yaml create mode 100755 package.rb create mode 100644 shard.lock create mode 100644 shard.yml create mode 100644 spec/asteroids_spec.cr create mode 100644 spec/spec_helper.cr create mode 100644 src/asteroids.cr create mode 100644 src/bullet.cr create mode 100644 src/lx_game/controller.cr create mode 100644 src/lx_game/game.cr create mode 100644 src/lx_game/graphics.cr create mode 100644 src/lx_game/sprite.cr create mode 100644 src/lx_game/vector_sprite.cr create mode 100644 src/ship.cr diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1811b58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf +/build/ diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..e6a6a92 --- /dev/null +++ b/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleGetInfoString + Asteroids + CFBundleExecutable + asteroids + CFBundleIdentifier + com.alexclink.asteroids + CFBundleName + Asteroids + CFBundleIconFile + asteroids.icns + CFBundleShortVersionString + 0.01 + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + IFMajorVersion + 0 + IFMinorVersion + 1 + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab4c30b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2021 Alex Clink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..10c59fc --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# asteroids + +TODO: Write a description here + +## Installation + +TODO: Write installation instructions here + +## Usage + +TODO: Write usage instructions here + +## Development + +TODO: Write development instructions here + +## Contributing + +1. Fork it () +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request + +## Contributors + +- [Alex Clink](https://github.com/your-github-user) - creator and maintainer diff --git a/lib-sdl.yaml b/lib-sdl.yaml new file mode 100644 index 0000000..b780e5e --- /dev/null +++ b/lib-sdl.yaml @@ -0,0 +1,10 @@ +name: LibSDL +ldflags: -lsdl2 +packages: sdl2 +destdir: src/lib_sdl +definitions: + sdl: + includes: + - SDL2/SDL.h + prefixes: + - sdl diff --git a/package.rb b/package.rb new file mode 100755 index 0000000..a478ae0 --- /dev/null +++ b/package.rb @@ -0,0 +1,84 @@ +#!/usr/bin/env ruby + +require 'fileutils' + +APP_NAME = 'Asteroids' +BUILD_DIR = "build/#{APP_NAME}.app/Contents" +BINARY = 'asteroids' + +def gather_libs(file) + libs = `otool -L #{file}`.lines + .map { |l| l[/^.+\.dylib/i]&.strip } + .reject do |l| + l.nil? || + l.empty? || + l.start_with?("/System/Library") || + l.start_with?("/usr/lib") || + l.start_with?("@rpath") + end + + libs.each do |l_path| + lib_name = File.basename(l_path) + dest_dir = "#{BUILD_DIR}/Frameworks" + dest_path = "#{dest_dir}/#{lib_name}" + + puts "#{lib_name}" + + if File.exist?("#{BUILD_DIR}/Frameworks/#{lib_name}") + puts " - Skipping #{lib_name}: Already present" + next + end + + unless File.exist? dest_path + begin + puts " - Copy #{l_path} to #{dest_path}" + FileUtils.cp l_path, dest_path + rescue Errno::EACCES => e + puts " - ERROR: #{l_path} cannot be copied: #{e}" + exit 1 + end + end + + `chown $(id -u):$(id -g) #{dest_path} && chmod +w #{dest_path}` + + unless $?.success? + puts "Could not change ownership and add write permissions to #{dest_path}" + exit 1 + end + + patch_cmd = "install_name_tool -change #{l_path} @executable_path/../Frameworks/#{lib_name} #{file}" + puts " - Patching executable link: \n - #{patch_cmd}" + `#{patch_cmd}` + unless $?.success? + exit 1 + end + + # Recursive copy + gather_libs dest_path + end +end + +puts "Creating structure for #{APP_NAME}:" +[ + BUILD_DIR, + "#{BUILD_DIR}/MacOS", + "#{BUILD_DIR}/Resources", + "#{BUILD_DIR}/Frameworks", +].each do |folder| + puts "Create: #{folder}" + FileUtils.mkdir_p folder +end + +puts "Copying Info.plist" +FileUtils.cp 'Info.plist', "#{BUILD_DIR}/" + +build_cmd = %{shards build --release --link-flags="-rpath @executable_path/../Frameworks -L #{`brew --prefix sfml`.chomp}/lib/ -mmacosx-version-min=10.14 -headerpad_max_install_names"} +puts "Building: `#{build_cmd}`" +puts `#{build_cmd}` + +FileUtils.cp "bin/#{BINARY}", "#{BUILD_DIR}/MacOS/#{BINARY}" + +puts "Gathering libs..." +gather_libs "#{BUILD_DIR}/MacOS/#{BINARY}" + +puts "Done!" \ No newline at end of file diff --git a/shard.lock b/shard.lock new file mode 100644 index 0000000..57a8e6b --- /dev/null +++ b/shard.lock @@ -0,0 +1,10 @@ +version: 2.0 +shards: + crystaledge: + git: https://github.com/unn4m3d/crystaledge.git + version: 0.2.6 + + sdl: + git: https://github.com/ysbaddaden/sdl.cr.git + version: 0.1.0+git.commit.d2aa0fb2ee30e42fc8d046d2d00c03c76879cb17 + diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..3067272 --- /dev/null +++ b/shard.yml @@ -0,0 +1,20 @@ +name: asteroids +version: 0.1.0 + +authors: + - Alex Clink + +targets: + asteroids: + main: src/asteroids.cr + +crystal: 1.2.1 + +license: MIT + +dependencies: + sdl: + github: ysbaddaden/sdl.cr + crystaledge: + github: unn4m3d/crystaledge + version: 0.2.6 diff --git a/spec/asteroids_spec.cr b/spec/asteroids_spec.cr new file mode 100644 index 0000000..1af3005 --- /dev/null +++ b/spec/asteroids_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Asteroids do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..be81617 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/asteroids" diff --git a/src/asteroids.cr b/src/asteroids.cr new file mode 100644 index 0000000..7194a09 --- /dev/null +++ b/src/asteroids.cr @@ -0,0 +1,69 @@ +require "sdl" +require "crystaledge" +include CrystalEdge +require "./lx_game/*" +include LxGame +require "./ship" +require "./bullet" + +class Asteroids < Game + @ship : Ship + @controller : Controller(LibSDL::Keycode) + + def initialize(*args) + super + + @ship = Ship.build do |ship| + ship.position = Vector2.new(x: width / 2.0, y: height / 2.0) + end + + @controller = Controller(LibSDL::Keycode).new({ + LibSDL::Keycode::UP => "Thrust", + LibSDL::Keycode::RIGHT => "Rotate Right", + LibSDL::Keycode::LEFT => "Rotate Left", + LibSDL::Keycode::SPACE => "Fire", + }) + + @bullets = [] of Bullet + end + + def wrap(position : Vector2) + position.x = 0.0 if position.x > @width + position.x = @width.to_f64 if position.x < 0.0 + + position.y = 0.0 if position.y > @height + position.y = @height.to_f64 if position.y < 0.0 + + position + end + + def update(dt : Float64) + @ship.rotate_right(dt) if @controller.action?("Rotate Right") + @ship.rotate_left(dt) if @controller.action?("Rotate Left") + @ship.thrust(dt) if @controller.action?("Thrust") + @ship.update(dt) + + @ship.position = wrap(@ship.position) + + if @controller.action?("Fire") && @ship.can_fire? + @bullets << @ship.fire + end + + @bullets.each do |bullet| + bullet.update + bullet.age += dt + end + + @bullets = @bullets.reject { |b| b.age >= 4.0 } + end + + def draw + @renderer.draw_color = SDL::Color[0, 0, 0, 255] + @renderer.clear + @ship.draw(@renderer) + @bullets.each { |b| b.draw(@renderer) } + end +end + +game = Asteroids.new(200, 120, 4) +game.run! diff --git a/src/bullet.cr b/src/bullet.cr new file mode 100644 index 0000000..0c7e488 --- /dev/null +++ b/src/bullet.cr @@ -0,0 +1,13 @@ +class Bullet < Sprite + property age : Float64 = 0.0 + + def update + @position += @velocity + end + + def draw(renderer) + brightness = ((4.0 - @age) / 4.0) * 255 + renderer.draw_color = SDL::Color[brightness, brightness, 0] + renderer.draw_point(@position.x.to_i, @position.y.to_i) + end +end diff --git a/src/lx_game/controller.cr b/src/lx_game/controller.cr new file mode 100644 index 0000000..edf080f --- /dev/null +++ b/src/lx_game/controller.cr @@ -0,0 +1,31 @@ +module LxGame + # Handle button to action mapping in a dynamic way + class Controller(T) + def initialize(@mapping : Hash(T, String)) + @keysdown = {} of String => Bool + + @mapping.values.each do |key| + @keysdown[key] = false + end + end + + def registered?(button) + @mapping.keys.includes?(button) + end + + def press(button) + return nil unless registered?(button) + @keysdown[@mapping[button]] = true + end + + def release(button) + return nil unless registered?(button) + @keysdown[@mapping[button]] = false + end + + # Returns duration of time pressed or false if not pressed + def action?(name) + @keysdown[name] + end + end +end diff --git a/src/lx_game/game.cr b/src/lx_game/game.cr new file mode 100644 index 0000000..3485b1f --- /dev/null +++ b/src/lx_game/game.cr @@ -0,0 +1,68 @@ +module LxGame + abstract class Game + FPS_INTERVAL = 1.0 + + property width : Int32 + property height : Int32 + property scale : Int32 + + @fps_lasttime : Float64 = Time.monotonic.total_milliseconds # the last recorded time. + @fps_current : UInt32 = 0 # the current FPS. + @fps_frames : UInt32 = 0 # frames passed since the last recorded fps. + @last_time : Float64 = Time.monotonic.total_milliseconds + + def initialize(@width, @height, @scale = 1) + SDL.init(SDL::Init::VIDEO) + at_exit { SDL.quit } + + @window = SDL::Window.new("SDL test", @width * @scale, @height * @scale) + @renderer = SDL::Renderer.new(@window, flags: SDL::Renderer::Flags::SOFTWARE) + @renderer.scale = {@scale, @scale} + end + + abstract def update(dt : Float64) + abstract def draw + + def elapsed_time + Time.monotonic.total_milliseconds + end + + def engine_update + @fps_frames += 1 + et = elapsed_time + + if @fps_lasttime < et - FPS_INTERVAL * 1000 + @fps_lasttime = et + @fps_current = @fps_frames + @fps_frames = 0 + @window.title = String.build { |io| io << "SDL test - " << @fps_current << " fps" } + end + + update((et - @last_time) / 1000.0) + @last_time = et + end + + def engine_draw + draw + @renderer.present + end + + def run! + loop do + case event = SDL::Event.poll + when SDL::Event::Keyboard + if event.keydown? + @controller.press(event.sym) + elsif event.keyup? + @controller.release(event.sym) + end + when SDL::Event::Quit + break + end + + engine_update + engine_draw + end + end + end +end diff --git a/src/lx_game/graphics.cr b/src/lx_game/graphics.cr new file mode 100644 index 0000000..411bff4 --- /dev/null +++ b/src/lx_game/graphics.cr @@ -0,0 +1,39 @@ +module LxGame + # Draw a line using Bresenham’s Algorithm + def draw_line(renderer : SDL::Renderer, p1 : Vector2, p2 : Vector2, draw_points = false) + return draw_line(renderer, p2, p1) if p1.x > p2.x + x1, y1, x2, y2 = p1.x.to_i, p1.y.to_i, p2.x.to_i, p2.y.to_i + + dx = (x2 - x1).abs + dy = -(y2 - y1).abs + + sx = x1 < x2 ? 1 : -1 + sy = y1 < y2 ? 1 : -1 + + d = dx + dy + x, y = x1, y1 + + loop do + renderer.draw_point(x, y) + break if x == x2 && y == y2 + + d2 = d + d + + if d2 >= dy + d += dy + x += sx + end + + if d2 <= dx + d += dx + y += sy + end + end + + if draw_points + renderer.draw_color = SDL::Color[255, 0, 0, 255] + renderer.draw_point(x1, y1) + renderer.draw_point(x2, y2) + end + end +end diff --git a/src/lx_game/sprite.cr b/src/lx_game/sprite.cr new file mode 100644 index 0000000..d721962 --- /dev/null +++ b/src/lx_game/sprite.cr @@ -0,0 +1,23 @@ +module LxGame + abstract class Sprite + def self.build + sprite = new + yield sprite + sprite + end + + property rotation : Float64 + property position : Vector2 + property velocity : Vector2 + property rotation_speed : Float64 + + def initialize + @position = Vector2.new(0.0, 0.0) + @velocity = Vector2.new(0.0, 0.0) + @rotation = 0.0 + @rotation_speed = 0.0 + end + + abstract def draw(renderer : SDL::Renderer) + end +end diff --git a/src/lx_game/vector_sprite.cr b/src/lx_game/vector_sprite.cr new file mode 100644 index 0000000..b4a82e7 --- /dev/null +++ b/src/lx_game/vector_sprite.cr @@ -0,0 +1,25 @@ +module LxGame + abstract class VectorSprite < Sprite + def project_points(points : Array(Vector2), rotation = @rotation, translate : Vector2? = nil, scale : Vector2? = nil) + rc = Math.cos(rotation) + rs = Math.sin(rotation) + + translation = + if t = translate + @position + t + else + @position + end + + points.map do |point| + rotated = Vector2.new(point.x * rc - point.y * rs, point.y * rc + point.x * rs) + + scale.try do |scale| + rotated = rotated * scale + end + + translation + rotated + end + end + end +end diff --git a/src/ship.cr b/src/ship.cr new file mode 100644 index 0000000..74932b7 --- /dev/null +++ b/src/ship.cr @@ -0,0 +1,98 @@ +class Ship < VectorSprite + getter frame : Array(Vector2) + @last_fired : Float64 + + def initialize + super + + @r_engine = 0.03 + @t_engine = 0.1 + + @frame = [ + Vector2.new(5.0, 0.0), + Vector2.new(-3.0, 3.0), + Vector2.new(-3.0, -3.0), + ] + + @jet_left = [Vector2.new(2.0, -5.0), Vector2.new(2.0, -3.5)] + @jet_right = [Vector2.new(2.0, 3.5), Vector2.new(2.0, 5.0)] + @jet_rear = [Vector2.new(-7.0, 0.0), Vector2.new(-3.0, 0.0)] + + @thrusting_left = false + @thrusting_right = false + @thrusting_forward = false + + @last_fired = Time.monotonic.total_milliseconds + end + + def can_fire? + now = Time.monotonic.total_milliseconds + now - @last_fired > 100.0 + end + + def fire + @last_fired = Time.monotonic.total_milliseconds + Bullet.build do |bullet| + bullet.position = project_points([@frame[0]]).first + bullet.velocity = @velocity + Vector2.new(Math.cos(@rotation), Math.sin(@rotation)) * 0.1 + end + end + + def rotate_right(dt : Float64, amount = @r_engine) + @thrusting_left = true + @rotation_speed += amount * dt + end + + def rotate_left(dt : Float64, amount = @r_engine) + @thrusting_right = true + @rotation_speed -= amount * dt + end + + def thrust(dt : Float64) + @thrusting_forward = true + @velocity.x += Math.cos(@rotation) * dt * @t_engine + @velocity.y += Math.sin(@rotation) * dt * @t_engine + end + + def update(dt : Float64) + @rotation += @rotation_speed + @position += @velocity + + # @rotation_speed = 0.0 if @rotation_speed < 0.001 && @rotation_speed > -0.001 + # rotate_left(dt, 0.01) if @rotation_speed >= 0.01 + # rotate_right(dt, 0.01) if @rotation_speed <= -0.01 + end + + def draw(renderer) + frame = project_points(@frame) + + renderer.draw_color = SDL::Color[128, 128, 128, 255] + + if @thrusting_left + @thrusting_left = false + jet_left = project_points(@jet_left, @rotation + rand(-0.5..0.5)) + draw_line(renderer, jet_left[0], jet_left[1]) + end + + if @thrusting_right + @thrusting_right = false + jet_right = project_points(@jet_right, @rotation + rand(-0.5..0.5)) + draw_line(renderer, jet_right[0], jet_right[1]) + end + + if @thrusting_forward + @thrusting_forward = false + jet_rear = project_points(@jet_rear, @rotation + rand(-0.5..0.5)) + draw_line(renderer, jet_rear[0], jet_rear[1]) + end + + renderer.draw_color = SDL::Color[255, 255, 255, 255] + + draw_line(renderer, frame[0], frame[1]) + draw_line(renderer, frame[1], frame[2]) + draw_line(renderer, frame[2], frame[0]) + + # renderer.draw_color = SDL::Color[255, 255, 0, 255] + # renderer.draw_point(@position.x.to_i, @position.y.to_i) + end +end