Initial Commit

This commit is contained in:
Alex Clink 2021-11-10 20:37:45 -05:00
commit 6d450f1871
19 changed files with 590 additions and 0 deletions

9
.editorconfig Normal file
View file

@ -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

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf
/build/

26
Info.plist Normal file
View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleGetInfoString</key>
<string>Asteroids</string>
<key>CFBundleExecutable</key>
<string>asteroids</string>
<key>CFBundleIdentifier</key>
<string>com.alexclink.asteroids</string>
<key>CFBundleName</key>
<string>Asteroids</string>
<key>CFBundleIconFile</key>
<string>asteroids.icns</string>
<key>CFBundleShortVersionString</key>
<string>0.01</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>IFMajorVersion</key>
<integer>0</integer>
<key>IFMinorVersion</key>
<integer>1</integer>
</dict>
</plist>

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Alex Clink <alexclink@gmail.com>
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.

27
README.md Normal file
View file

@ -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 (<https://github.com/your-github-user/asteroids/fork>)
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

10
lib-sdl.yaml Normal file
View file

@ -0,0 +1,10 @@
name: LibSDL
ldflags: -lsdl2
packages: sdl2
destdir: src/lib_sdl
definitions:
sdl:
includes:
- SDL2/SDL.h
prefixes:
- sdl

84
package.rb Executable file
View file

@ -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!"

10
shard.lock Normal file
View file

@ -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

20
shard.yml Normal file
View file

@ -0,0 +1,20 @@
name: asteroids
version: 0.1.0
authors:
- Alex Clink <alexclink@gmail.com>
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

9
spec/asteroids_spec.cr Normal file
View file

@ -0,0 +1,9 @@
require "./spec_helper"
describe Asteroids do
# TODO: Write tests
it "works" do
false.should eq(true)
end
end

2
spec/spec_helper.cr Normal file
View file

@ -0,0 +1,2 @@
require "spec"
require "../src/asteroids"

69
src/asteroids.cr Normal file
View file

@ -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!

13
src/bullet.cr Normal file
View file

@ -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

31
src/lx_game/controller.cr Normal file
View file

@ -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

68
src/lx_game/game.cr Normal file
View file

@ -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

39
src/lx_game/graphics.cr Normal file
View file

@ -0,0 +1,39 @@
module LxGame
# Draw a line using Bresenhams 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

23
src/lx_game/sprite.cr Normal file
View file

@ -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

View file

@ -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

98
src/ship.cr Normal file
View file

@ -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