Compare commits

...

25 Commits

Author SHA1 Message Date
3469024b23 WIP events & handler
This change tries to modivy the event to be a "generic entity" that
holds the data about the event (type, code, modifiers).

It does not use crystal generics.

I don't plan to merge it on master because it adds a overheat.
2023-08-24 19:19:53 +02:00
8012ff40af ui: properize and test the ui event handler 2023-08-23 22:24:06 +02:00
c48d350f58 sandbox: parameterize game constants 2023-08-23 21:40:34 +02:00
01cc102d88 engine: move movingbody to engine and add specs 2023-08-23 21:27:55 +02:00
c4012f2400 sandbox: add callback system for low thrust 2023-08-23 19:17:01 +02:00
873703e7ce sandbox: move more code around to structure the code 2023-08-23 18:36:42 +02:00
81b00b576b sandbox: move some code around to structure the code 2023-08-23 18:32:12 +02:00
3a47b165f2 sandbox: manual digital vector input is validated by buttons comfirm/reset/zero 2023-08-23 10:14:46 +02:00
b2f98faa93 gravity: quick hack 2023-08-22 22:04:12 +02:00
8a9845807e physics: fix self-gravity effect 2023-08-22 18:34:44 +02:00
3f93a7729b sandbox: add moving bodies and factorize sf shape 2023-08-22 18:27:18 +02:00
56b4b139f1 vector: fixes to_s 2023-08-22 17:57:51 +02:00
10cc4e3b96 vector: add zero vector 2023-08-22 17:38:57 +02:00
70af8da4a4 gravity: move gravity computations in the engine 2023-08-22 10:26:47 +02:00
7b7501d61c sandbox: improve interface 2023-08-21 22:59:53 +02:00
2216222a8b sandbox: add bodies inside gravitic field 2023-08-21 22:41:41 +02:00
35db808f20 sandbox: add gravity field and star sling 2023-08-21 22:19:23 +02:00
ec4348b652 vector: add div 2023-08-21 22:09:07 +02:00
30f4712da8 vector: add norm and magnitude 2023-08-21 18:54:33 +02:00
09be5680a4 sandbox: add input digital control for thrusters 2023-08-21 17:45:23 +02:00
fb6a8a4df4 sandbox: add controls and sliders for acceleration 2023-08-21 12:22:50 +02:00
978eb648e2 Add a sandbox to for PoC 2023-08-20 19:39:21 +02:00
ad8d62556e Add Vector#mult! 2023-08-20 13:17:56 +02:00
7505fcfac7 Add basic space physics 2023-08-20 13:13:37 +02:00
2d999847b7 Add simple vectors 2023-08-20 10:49:50 +02:00
22 changed files with 1319 additions and 3 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
/zero_epsilon
/lib
/imgui.ini
/docs
/physics_sandbox

View File

@ -13,7 +13,7 @@ debug:
release:
crystal build src/$(NAME).cr --stats --release
test:
crystal spec
crystal spec --error-trace
deps:
shards install
deps_update:
@ -21,7 +21,7 @@ deps_update:
deps_opt:
@[ -d lib/ ] || make deps
doc:
crystal docs ./src/core.cr
crystal docs ./src/zero_epsilon.cr ./src/tests/physics_sandbox.cr ./lib/crsfml/src/crsfml.cr ./lib/imgui/src/imgui.cr
clean:
rm $(NAME)

View File

@ -23,6 +23,9 @@ After you build the game, run it with
## Contributing
You can send email or join me on irc.
Details on <https://sceptique.eu>.
Details on <https://sceptique.eu/about>.
If you already have an account on git.sceptique.eu you can contribute to the upstream <https://git.sceptique.eu/Sceptique/ZeroEpsilon>.
I accept all kinds of patch as long as it's not bullshit.
If you want to test the sandbox, you can try `crystal run src/tests/physics_sandbox.cr --error-trace`

14
shard.lock Normal file
View File

@ -0,0 +1,14 @@
version: 2.0
shards:
crsfml:
git: https://github.com/oprypin/crsfml.git
version: 2.5.3
imgui:
git: https://github.com/oprypin/crystal-imgui.git
version: 1.89.5
imgui-sfml:
git: https://github.com/oprypin/crystal-imgui-sfml.git
version: 1.89.5

View File

@ -0,0 +1,15 @@
require "../../../src/engine/gravity/body"
describe Gravity::Body do
it "accelerate arbitrary points" do
earth = Gravity::Body(2).new(mass: 5972200000000000000000000.0, position: Vector[0.0, 0.0])
earth = Gravity::Body.new(mass: 5972200000000000000000000.0, position: Vector[0.0, 0.0])
earth_surface = Vector[6371000.0, 0.0]
earth.acceleration(earth_surface).magnitude.round(2).should eq(9.82)
end
it "has acceleration and speed but null" do
Gravity::Body.new(mass: 123, position: Vector[1.0, 2.0]).speed.should eq(Vector(Float64, 2).zero)
Gravity::Body.new(mass: 123, position: Vector[1.0, 2.0, 3.0]).speed.should eq(Vector(Float64, 3).zero)
end
end

View File

@ -0,0 +1,12 @@
require "../../../src/engine/gravity/field"
describe Gravity::Field do
it "accelerate arbitrary points" do
earth = Gravity::Body(2).new(mass: 5972200000000000000000000.0, position: Vector[0.0, 0.0])
field = Gravity::Field(2).new([earth])
field = Gravity::Field(2).new(earth)
field = Gravity::Field(2).new({earth})
earth_surface = Vector[6371000.0, 0.0]
earth.acceleration(earth_surface).magnitude.round(2).should eq(9.82)
end
end

View File

@ -0,0 +1,14 @@
require "../../../src/engine/gravity/moving_body"
describe Gravity::MovingBody do
it "accelerate arbitrary points" do
earth = Gravity::MovingBody.new(
mass: 5972200000000000000000000.0,
position: Vector[0.0, 0.0],
acceleration: Vector[1.0, 0.0],
speed: Vector[2.0, 0.0],
)
earth.acceleration.should eq(Vector[1.0, 0.0])
earth.speed.should eq(Vector[2.0, 0.0])
end
end

111
spec/engine/physics_spec.cr Normal file
View File

@ -0,0 +1,111 @@
require "spec"
require "../../src/engine/physics"
describe Physics do
it "test basic tick usage" do
time = Time.utc
one_second = Time::Span.new(seconds: 1)
t1 = Physics::Tick.new(n: 0, time: time, last: nil)
t2 = Physics::Tick.new(n: 1, time: time + one_second, last: t1)
t2.timelaps.should eq(one_second)
end
it "test basic timer" do
timer = Physics::Tick::Timer.new
start_time = timer.tick.time
timer.tick.n.should eq(0)
12.times { timer.timeskip!(seconds: 1) }
timer.tick.n.should eq(12)
timer.tick.time.should eq(start_time + Time::Span.new(seconds: 12))
end
it "accelerate an object" do
a_ball_movement = Vector[0.0, 0.0, 0.0]
a_engine_accelerate = Vector[20.0, 0.0, 0.0]
timer = Physics::Tick::Timer.new
timer.timeskip!(seconds: 1)
Physics.accelerate_by_one_tick!(timer.tick, a_ball_movement, a_engine_accelerate)
a_ball_movement[0].should eq(20.0)
a_ball_movement[1].should eq(0.0)
timer.timeskip!(milliseconds: 500)
Physics.accelerate_by_one_tick!(timer.tick, a_ball_movement, a_engine_accelerate)
a_ball_movement[0].should eq(30.0)
a_ball_movement[1].should eq(0.0)
timer.timeskip!(milliseconds: 500)
Physics.accelerate_by_one_tick!(timer.tick, a_ball_movement, a_engine_accelerate)
a_ball_movement[0].should eq(40.0)
a_ball_movement[1].should eq(0.0)
a_engine_stopped = Vector[0.0, 0.0, 0.0]
timer.timeskip!(milliseconds: 500)
Physics.accelerate_by_one_tick!(timer.tick, a_ball_movement, a_engine_stopped)
a_ball_movement[0].should eq(40.0)
a_ball_movement[1].should eq(0.0)
a_retro_pulse = Vector[0.0, 0.1, 0.0]
timer.timeskip!(milliseconds: 500)
Physics.accelerate_by_one_tick!(timer.tick, a_ball_movement, a_retro_pulse)
a_ball_movement[0].should eq(40.0)
a_ball_movement[1].should eq(0.05)
end
it "move and accelerate an object" do
a_ball_position = Vector[0.0, 0.0, 0.0]
a_ball_movement = Vector[0.0, 0.0, 0.0]
a_engine_accelerate = Vector[20.0, 0.0, 0.0]
timer = Physics::Tick::Timer.new
timer.timeskip!(seconds: 1)
Physics.move_by_one_tick!(timer.tick, a_ball_position, a_ball_movement, a_engine_accelerate)
a_ball_position[0].should eq(10.0)
a_ball_position[1].should eq(0.0)
a_ball_movement[0].should eq(20.0)
a_ball_movement[1].should eq(0.0)
timer.timeskip!(seconds: 1)
Physics.move_by_one_tick!(timer.tick, a_ball_position, a_ball_movement, a_engine_accelerate)
a_ball_position[0].should eq(40.0)
a_ball_position[1].should eq(0.0)
a_ball_movement[0].should eq(40.0)
a_ball_movement[1].should eq(0.0)
# cut a second in 2 computations gives the same result as one second
timer.timeskip!(milliseconds: 500)
Physics.move_by_one_tick!(timer.tick, a_ball_position, a_ball_movement, a_engine_accelerate)
a_ball_position[0].should eq(62.5)
a_ball_position[1].should eq(0.0)
a_ball_movement[0].should eq(50.0)
a_ball_movement[1].should eq(0.0)
timer.timeskip!(milliseconds: 500)
Physics.move_by_one_tick!(timer.tick, a_ball_position, a_ball_movement, a_engine_accelerate)
a_ball_position[0].should eq(90.0)
a_ball_position[1].should eq(0.0)
a_ball_movement[0].should eq(60.0)
a_ball_movement[1].should eq(0.0)
a_gravity_generator = Vector[0.0, 0.0, 0.0]
a_retro_pulse = Vector[0.0, 0.1, 0.0]
star_gravity = Vector[-0.01, 0.02, 0.01]
total_forces = a_gravity_generator + a_retro_pulse + star_gravity
timer.timeskip!(seconds: 1)
Physics.move_by_one_tick!(timer.tick, a_ball_position, a_ball_movement, total_forces)
a_ball_position[0].round(3).should eq(149.995)
a_ball_position[1].round(3).should eq(0.06)
a_ball_position[2].round(3).should eq(0.005)
a_ball_movement[0].round(3).should eq(59.99)
a_ball_movement[1].round(3).should eq(0.12)
a_ball_movement[2].round(3).should eq(0.01)
end
end
# v0 = 40 m/s
# a = 20 m/s²
# t1 = 0.5 s
# v1 = v0 + a * t1 = 40 + 20 * 0.6 = 50 m/s (m/s + m/s^2*s = m/s)
# [v] = (v0 + v1) / 2 = (40 + 50) / 2 = 45 (m/s + m/s)
# d = [v] * (t1 - t0) = 45 * 0.5 = 22.5 (m/s * s)

140
spec/engine/vector_spec.cr Normal file
View File

@ -0,0 +1,140 @@
require "spec"
require "../../src/engine/vector"
describe Vector do
it "allocate vectors" do
Vector[1i64, 2i64, 3i64, 4i64]
Vector[1i32, 2i64, 3i64, 4i64]
Vector[4i64]
Vector[1, 2.0, 3u64, 4.1f64]
v = Vector(Int32, 4).new { |i| i + 1 }
v[0].should eq(1)
v[1].should eq(2)
v[2].should eq(3)
v[3].should eq(4)
v06 = Vector(Float64, 6).zero
v06[0].should eq(0.0)
v06.size.should eq(6)
v06.tuple_type.should eq(Float64)
end
it "has a size" do
Vector[1].size.should eq(1)
Vector[1, 2].size.should eq(2)
Vector[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.0].size.should eq(11)
end
it "has a tuple_type" do
Vector[1].tuple_type.should eq(Int32)
Vector[1, 2].tuple_type.should eq(Int32)
Vector[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.0].tuple_type.should eq(Int32 | Float64)
end
it "equality test" do
(Vector[1, 2] == Vector[1, 2]).should be_true
(Vector[1, 2] != Vector[2, 2]).should be_true
(Vector[1, 2, 3] != Vector[1, 2]).should be_true
(Vector[1, 2] != Vector[1, 2, 3]).should be_true
(Vector[1, 2] != Vector[1, 2, 0]).should be_true
end
it "clones vectors" do
v1 = Vector[1, 2, 3]
v2 = v1.clone
v1.to_unsafe[0] = 0
v1[0].should_not eq(v2[0])
v1[1].should eq(v2[1])
end
it "mutates adds vectors" do
v1 = Vector[1, 2]
v2 = Vector[0, 1]
v3 = v1.add!(v2)
v1.object_id.should eq(v3.object_id)
v1[0].should eq(1)
v1[1].should eq(3)
v2[0].should eq(0)
v2[1].should eq(1)
end
it "adds vectors" do
v1 = Vector[1, 2]
v2 = Vector[0, 1]
v3 = v1 + v2
v1[0].should eq(1)
v2[0].should eq(0)
v3[0].should eq(1)
v1[1].should eq(2)
v2[1].should eq(1)
v3[1].should eq(3)
end
it "negates a vector" do
v1 = Vector[1, 2]
v2 = -v1
v1[0].should eq(1)
v1[1].should eq(2)
v2[0].should eq(-1)
v2[1].should eq(-2)
end
it "substract vectors" do
v1 = Vector[1, 2]
v2 = Vector[2, 1]
v3 = v1 - v2
v1[0].should eq(1)
v2[0].should eq(2)
v3[0].should eq(-1)
v1[1].should eq(2)
v2[1].should eq(1)
v3[1].should eq(1)
end
it "mutate multiply vector" do
v1 = Vector[1, 2]
v2 = v1.mult!(2)
v2.object_id.should eq(v1.object_id)
v1[0].should eq(2)
v1[1].should eq(4)
end
it "multiply vector" do
v1 = Vector[1, 2]
v2 = v1 * 2
v1[0].should eq(1)
v1[1].should eq(2)
v2[0].should eq(2)
v2[1].should eq(4)
end
# it "mutate divide vector" do
# v1 = Vector[5.0, 21]
# v2 = v1.div!(2)
# v2.object_id.should eq(v1.object_id)
# v1[0].should eq(2.5)
# v1[1].should eq(10)
# end
it "divide vector" do
v1 = Vector[5.0, 21]
v2 = v1 / 2
v1[0].should eq(5.0)
v1[1].should eq(21)
v2[0].should eq(2.5)
v2[1].should eq(10)
end
it "magnitude of vector" do
Vector[1, 1].magnitude.should eq(Math.sqrt(2))
Vector[1, 2].magnitude.should eq(Math.sqrt(1**2 + 2**2))
Vector[2, 2].magnitude.should eq(Math.sqrt(2**2 + 2**2))
Vector[2, 2, 2].magnitude.should eq((2**2 + 2**2 + 2**2)**(1.0/3.0))
end
it "normalized vector" do
Vector[1].normalize == Vector[1]
Vector[2].normalize == Vector[1]
Vector[2.3].normalize == Vector[1.0]
(Vector[1, 2].normalize == Vector[1 / Math.sqrt(5), 2 / Math.sqrt(5)])
end
end

View File

@ -0,0 +1,23 @@
require "spec"
require "../src/frequency_loop"
describe FrequencyLoop do
it "testes basic looping" do
fl = FrequencyLoop.new(Time::Span.new(nanoseconds: 1_000))
size = 5
i = 0
t0 = Time.utc
fl.loop(max_tick: size) { i += 1 }
t1 = Time.utc
i.should eq(size)
full_span = t1 - t0
# puts "full_span=#{full_span}"
# puts "10nano=#{Time::Span.new(nanoseconds: size * 1_000)}"
# puts "sleep=#{fl.sleep}"
# puts "span=#{fl.span}"
((t1 - t0) > Time::Span.new(nanoseconds: size * 1_000)).should be_true
end
end

View File

@ -0,0 +1,79 @@
require "../../src/ui/event_handler"
class TestingEvent; end
class TestingEventA < TestingEvent
getter :code, :alt, :control, :shift, :system, :stuff
def initialize(@code = "001", @alt = false, @control = false, @shift = false, @system = false, @stuff = false)
end
end
class TestingEventB < TestingEvent
getter :code
def initialize(@code = "001")
end
end
class UI::Event
TEST_MAPPING = { TestingEventA => Type::KeyPressed, TestingEventB => Type::KeyReleased, }
def initialize(event : TestingEvent)
@type = TEST_MAPPING[event.class]? || Type::Unknown
@code = event.responds_to?(:code) ? event.code : nil
@alt = event.responds_to?(:alt) ? event.alt.to_s : nil
@control = event.responds_to?(:control) ? event.control.to_s : nil
@shift = event.responds_to?(:shift) ? event.shift.to_s : nil
@system = event.responds_to?(:system) ? event.system.to_s : nil
end
class Handler
def event_callbacks(event : TestingEvent.class)
event = TEST_MAPPING[event]
event_callbacks(event)
end
def handle(event : TestingEvent)
handle(Event.new(event))
end
end
end
describe UI::Event::Handler do
it "verifies event accumulates and remove callbacks" do
a = UI::Event::Handler.new
counter = 0
a.add(UI::Event::Type::KeyPressed) do |cb, _|
counter += 1
end
a.add(UI::Event::Type::KeyReleased) do |cb, _|
counter += 10
end
# test having multiple events with different types
counter.should eq(0)
a.handle(TestingEventA.new)
counter.should eq(1)
a.handle(TestingEventA.new)
counter.should eq(2)
a.handle(TestingEventB.new)
counter.should eq(12)
# test 2 callbacks on the same event
a.add(UI::Event::Type::KeyPressed) do |cb, _|
counter += 1
end
a.handle(TestingEventA.new)
counter.should eq(14)
# test a callback with autoremove (useful for keypressing)
a.add(UI::Event::Type::KeyPressed) do |cb, _|
counter += 100
a.remove(UI::Event::Type::KeyPressed, cb)
end
a.handle(TestingEventA.new)
counter.should eq(116)
a.handle(TestingEventA.new)
counter.should eq(118)
end
end

1
src/engine/gravity.cr Normal file
View File

@ -0,0 +1 @@
require "./gravity/*"

View File

@ -0,0 +1,40 @@
module Gravity
# A `Gravity::Body` simulates a heavy object that generate a significant warp of space time.
# Several bodies can be combined in a single `Gravity::Field`.
# A body exists in a space of N dimensions
# and all vectors related to the body must have exactly N dimensions too.
class Body(N)
G = 6.6743*10**-11.0 # m³/(kg*s²)
EARTH2 = Gravity::Body(2).new(mass: 5972200000000000000000000.0, position: Vector(Float64, 2).zero)
EARTH3 = Gravity::Body(3).new(mass: 5972200000000000000000000.0, position: Vector(Float64, 3).zero)
getter :mass, :position, :g
def initialize(@mass : Float64, @position : Vector(Float64, N) = Vector(Float64, N).zero, @g : Float64 = G)
end
def acceleration(position : Vector(Float64, N)) : Vector(Float64, N)
return Vector(Float64, N).zero if position == @position
orientation = @position - position
distance = orientation.magnitude
magnitude = @g * @mass / (distance ** 3)
# original is magnitude = g * m / d² but since we also need to make
# orientation a magnitude 1 vector to multiply it with magnitude later
# I quicken the computation here with a single ** 3 operation rather than /d**2 then /d again
orientation.mult! magnitude
orientation
end
# Non-moving bodies don't accelerate
def acceleration
Vector(Float64, N).zero
end
# Non-moving bodies don't move
def speed
Vector(Float64, N).zero
end
end
end

View File

@ -0,0 +1,39 @@
require "./body"
require "../vector"
module Gravity
# A `Gravity::Field` contains several `Gravity::Body` that warp space-time.
# It accelerates any point given to it using `#acceleration`
# A gravity field exists in N dimensions.
# All vectors and body must also be N dimensional.
class Field(N)
getter bodies : Array(Body(N))
# standard constructor
def initialize(@bodies : Array(Body(N)))
end
# standard constructor
def initialize(bodies : Tuple(Body(N)))
@bodies = bodies.to_a
end
# helping constructor for testing and sandboxes
# - bodies must be a set of `Gravity::Body`
def initialize(*bodies)
@bodies = bodies.to_a
end
# Generate an acceleration vector based on all bodies of the gravity field
def acceleration(position : Vector(T, N)) : Vector(T, N) forall T
acceleration_vector = position.class.zero
# acceleration_vector = Vector[0.0, 0.0]
@bodies.reduce(acceleration_vector) do |base, body|
add_vector = body.acceleration(position)
base.add!(add_vector)
base
end
acceleration_vector
end
end
end

View File

@ -0,0 +1,10 @@
require "./body"
module Gravity
class MovingBody(N) < Body(N)
getter :acceleration, :speed
def initialize(@acceleration : Vector(Float64, N) = Vector(Float64, N).zero, @speed : Vector(Float64, N) = Vector(Float64, N).zero, *p, **o)
super(*p, **o)
end
end
end

53
src/engine/physics.cr Normal file
View File

@ -0,0 +1,53 @@
require "./vector"
module Physics
class Tick
property :n, :time, :last
def initialize(@n : Int64, @time : Time, @last : Tick? = nil)
end
def timelaps : Time::Span
last = @last
if last.nil?
return Time::Span.new
else
return @time - last.time
end
end
class Timer
getter :tick
def initialize
@tick = Tick.new(n: 0, time: Time.utc, last: nil)
end
def timeskip!(seconds : Int = 0, milliseconds : Int = 0, nanoseconds : Int = 0)
timeskip!(timespan: Time::Span.new(seconds: seconds, nanoseconds: nanoseconds + milliseconds * 1_000_000))
end
def timeskip!(timespan : Time::Span)
@tick = Tick.new(n: @tick.n + 1, time: @tick.time + timespan, last: @tick)
self
end
end
end
def self.accelerate_by_one_tick!(tick : Tick, speed : Vector(T, N), acceleration : Vector(T, N)) : Vector(T, N) forall T, N
fraction = tick.timelaps.total_milliseconds / 1000.0
speed.add!(acceleration * fraction)
end
def self.move_by_one_tick!(tick : Tick, position : Vector(T, N), speed : Vector(T, N), acceleration : Vector(T, N)) : Vector(T, N) forall T, N
time_fraction = tick.timelaps.total_milliseconds / 1000.0
# TODO optimise memory using mutation of intermediate values
max_speed = speed + (acceleration * time_fraction)
tick_average_speed = (speed + max_speed) * 0.5 * time_fraction
position.add!(tick_average_speed)
speed.add!(acceleration * time_fraction)
end
end

167
src/engine/vector.cr Normal file
View File

@ -0,0 +1,167 @@
# # this is a sloppy monkey patch that only works because I don't manipulate types in a complex way
# struct Int
# def clear_div(right : Number)
# self // right
# end
# end
# struct Float
# def clear_div(right : Number)
# self / right
# end
# end
# alias Vector = Slice
class Vector(T, N) # should be a struct since it's a simple pointer inside, the rest is compile time
@buffer : Slice(T)
include Indexable::Mutable(T)
def initialize(@buffer)
end
def self.new(&block : Int32 -> T)
pointer = Pointer(T).malloc(N, &block)
slice = Slice.new(pointer, N)
Vector(T, N).new(slice)
end
def self.zero
new { T.zero }
end
macro [](*args)
%args_type = typeof({{*args}})
%args_type_size = sizeof(typeof({{*args}}))
%args_size = {{args.size}}
%pointer = Pointer(typeof({{*args}})).malloc(%args_size) do |i|
({{args}})[i]
end
%slice = Slice.new(%pointer, %args_size)
Vector(typeof({{*args}}), {{args.size}}).new(%slice)
end
def pretty_print(pp)
pp.list("Vector[", @buffer, "]")
end
def to_s(io : IO) : Nil
io << "Vector["
join io, ", ", &.inspect(io)
io << ']'
end
def size
N
end
@[AlwaysInline]
def unsafe_fetch(index : Int) : T
@buffer[index]
end
@[AlwaysInline]
def unsafe_put(index : Int, value : T)
@buffer[index] = value
end
def !=(right : Vector(U, N)) forall U
size.times do |index|
return true if to_unsafe[index] != right.to_unsafe[index]
end
return false
end
def ==(right : Vector(U, N)) forall U
!(self != right)
end
def add!(right : Vector(U, N)) forall U
size.times do |index|
@buffer[index] += right[index]
end
self
end
def clone
# note: can be optimized using Slice#copy
Vector(T, N).new { |index| self[index] }
end
def +(right : Vector(U, N)) forall U
clone = self.clone
size.times do |index|
clone.to_unsafe[index] += right[index]
end
clone
end
def -()
clone = self.clone
size.times do |index|
clone.to_unsafe[index] = -clone.to_unsafe[index]
end
clone
end
def -(right : Vector(U, N)) forall U
clone = self.clone
size.times do |index|
clone.to_unsafe[index] -= right[index]
end
clone
end
def mult!(right : Number)
size.times do |index|
@buffer[index] *= right
end
self
end
def div!(right : Number)
size.times do |index|
klass = @buffer[index].class
@buffer[index] =
klass.new(
if klass < Float
@buffer[index] / right
elsif klass < Int
@buffer[index] // right
else
@buffer[index] / right
end
)
end
self
end
def *(right : Number)
clone = self.clone
clone.mult!(right)
clone
end
def /(right : Number)
clone = self.clone
clone.div!(right)
clone
end
def to_unsafe
@buffer
end
def tuple_type
T
end
# I think it's always square root not 1.0/size
def magnitude : Number
reduce(T.zero) { |base, elem| base + elem**2 } ** (1.0 / size)
end
def normalize
magnitude = self.magnitude
self / magnitude
end
end

24
src/frequency_loop.cr Normal file
View File

@ -0,0 +1,24 @@
class FrequencyLoop
def initialize(@frequency : Time::Span)
@sleep = [] of Time::Span
@span = [] of Time::Span
end
getter :sleep, :frequency, :span
ZERO = Time::Span.zero
def loop(max_tick : Int = UInt64::MAX, &)
i = 0u64
t0 = Time.utc
while max_tick > i
yield i
t1 = Time.utc
span = (t1 - t0)
i += 1
sleep_span = (@frequency * i) - span
@sleep << sleep_span
@span << span
sleep(sleep_span) if sleep_span > ZERO
end
end
end

102
src/imgui_helper.cr Normal file
View File

@ -0,0 +1,102 @@
module ImGui::Helper
# @example
# draw_table(title: "manpower", headers: {"Infra", "Ajust", "Lock"}) do
# infra_hash.keys.each do |infra_id, infra|
# draw_line(infra)
# end
# end
def draw_table(title : String, headers, &block)
columns_amount = headers.size
if ImGui.begin_table(title, columns_amount)
ImGui.table_next_row
ImGui.table_next_column
headers.each do |header|
ImGui.text header
ImGui.table_next_column
end
yield
ImGui.end_table
end
end
# @example
# v = storage[someid]
# ptr = pointerof(v)
# draw_table_line(
# someid,
# ->{
# if ImGui.slider_float(
# label: "absolute####{infra.id}",
# v: ptr,
# v_min: 0.0f32,
# v_max: 10.0f32,
# flags: (
# ImGui::ImGuiSliderFlags::NoRoundToFormat |
# ImGui::ImGuiSliderFlags::Logarithmic
# ),
# )
# store[someid] = v
# end
# },
# "some labels",
# )
def draw_table_line(*columns : String | Proc)
draw_table_line
columns.each do |column|
draw_table_cell(column)
end
end
# @example
# draw_table_line
# draw_table_cell id
# draw_table_cell name
# draw_table_cell somevalue
# draw_table_line
#
def draw_table_line
ImGui.table_next_row
# print "\n"
end
# @example
# draw_table_line
# draw_table_cell id
# draw_table_cell -> { ImGui.text "label" }
def draw_table_cell(cell : String | Proc)
draw_table_cell
if cell.is_a?(String)
# print cell
ImGui.text cell
else
# print cell.call
cell.call
end
end
# @example
# draw_table_cell do
# someaction(someid) if ImGui.button("build####{someid}")
# end
def draw_table_cell(&block)
draw_table_cell
# print "YIELD"
yield
end
# @example
# draw_table_line
# draw_table_cell
# ImGui.text cell
def draw_table_cell
ImGui.table_next_column
# print "|"
end
# def draw_table_next_line
# ImGui.table_next_row
# end
end

View File

@ -0,0 +1,348 @@
require "../engine/*"
require "../ui/*"
require "../frequency_loop"
require "../imgui_helper"
require "crsfml"
require "imgui"
require "imgui-sfml"
require "log"
Log.setup(:debug)
class Log
def self.debug(str)
Log.debug { str }
end
end
# TODO move this to engine
class Projectable(N)
property :gravity_body, :color, :size, :outline_color, :outline_size
def initialize(
@gravity_body : Gravity::Body(N),
@color : Tuple(Int32, Int32, Int32),
@size : Int32,
@outline_color : Tuple(Int32, Int32, Int32) = {0, 0, 0},
@outline_size : Int32 = 0,
)
end
def position
@gravity_body.position
end
def acceleration
@gravity_body.acceleration
end
def speed
@gravity_body.speed
end
def sf_shape
shape = SF::CircleShape.new(@size)
shape.fill_color = SF.color(@color[0], @color[1], @color[2])
shape.outline_thickness = @outline_size
shape.outline_color = SF.color(@outline_color[0], @outline_color[1], @outline_color[2])
shape.position = sf_position(0.5)
shape
end
def sf_position(coef : Float64 = 1.0)
SF.vector2(@gravity_body.position[0] * coef, @gravity_body.position[1] * coef)
end
end
class Game
@window : SF::RenderWindow
@frequency_nano_rate : Int32
@delta_clock : SF::Clock
def initialize(framerate = 60, width = 800, height = 600)
@window = SF::RenderWindow.new(
SF::VideoMode.new(width, height),
"Sandbox",
)
@delta_clock = SF::Clock.new
ImGui::SFML.init(@window)
@window.framerate_limit = framerate
@frequency_nano_rate = 1_000_000_000 // framerate
@frequency = Time::Span.new(nanoseconds: @frequency_nano_rate)
@ui_states = {
acceleration_digit: {
:x => 0.0,
:y => 0.0,
},
acceleration_log_analogic_pressed: [false], # note: I should probably use pointers instead of this shit
acceleration_log_analogic_save: [0.0],
}
@bodies = {
vessel: Gravity::MovingBody(2).new(mass: 1.0, g: 0.1, position: Vector[10.0, 10.0]),
star: Gravity::MovingBody(2).new(mass: 300000.0, g: 0.1, position: Vector[400.0, 400.0]),
pla1: Gravity::MovingBody(2).new(mass: 15000.0, g: 0.1, position: Vector[200.0, 200.0]),
pla2: Gravity::MovingBody(2).new(mass: 18000.0, g: 0.1, position: Vector[280.0, 320.0]),
}
@vessel = Projectable(2).new(
gravity_body: @bodies[:vessel],
color: { 255, 0, 250 },
size: 3,
)
@star = Projectable(2).new(
gravity_body: @bodies[:star],
color: { 200, 200, 50 },
size: 10,
)
@pla1 = Projectable(2).new(
gravity_body: @bodies[:pla1],
color: { 150, 50, 50 },
size: 5,
)
@pla2 = Projectable(2).new(
gravity_body: @bodies[:pla2],
color: { 50, 100, 150 },
size: 5,
)
@g = Gravity::Field(2).new([@star.gravity_body, @pla1.gravity_body, @pla2.gravity_body])
@timer = Physics::Tick::Timer.new
@ui_events_handler = UI::EventHandler.new
end
def handle_events
while event = @window.poll_event
ImGui::SFML.process_event(@window, event)
case event
when SF::Event::Closed
@window.close
exit
when SF::Event::KeyPressed
puts "KeyPressed #{event}"
when SF::Event::MouseButtonEvent
puts "MouseButtonEvent #{event}"
end
@ui_events_handler.handle(event)
end
end
def execute_loop
while @window.open? # restart the loop when if it ends
FrequencyLoop.new(@frequency).loop do |i|
@window.clear(SF::Color::Black)
handle_events
ImGui::SFML.update(@window, @delta_clock.restart)
execute(i)
ImGui::SFML.render(@window)
@window.display
end
end
end
include ImGui::Helper
# will yield the value
def draw_slider(label : String, data : Float64, max : Float64 = 100.0, min : Float64 = -100.0, flags : ImGui::ImGuiSliderFlags = ImGui::ImGuiSliderFlags::NoRoundToFormat, &)
ptr = pointerof(data)
if ImGui.slider_scalar(
label: label,
p_data: ptr,
p_min: min,
p_max: max,
flags: flags,
)
yield data
end
end
# will yield the value
def draw_input(label : String, data : Float64, max : Float64 = 100.0, min : Float64 = -100.0, flags : ImGui::ImGuiInputTextFlags = ImGui::ImGuiInputTextFlags::EnterReturnsTrue, &)
ptr = pointerof(data)
if ImGui.input_scalar(
label: label,
p_data: ptr,
flags: flags,
)
data = max if data > max
data = min if data < min
yield data
end
end
def accelerate_by_tick(movable_body : Gravity::MovingBody)
gravity_field = @g.acceleration(movable_body.position)
acceleration = movable_body.acceleration + gravity_field
Physics.move_by_one_tick!(@timer.tick, movable_body.position, movable_body.speed, acceleration)
end
def draw_pilot
draw_table(title: "Trusters", headers: {"Name", "X", "Y", "unit"}) do
draw_table_line(
"Analogic acceleration",
-> {
draw_slider(
label: "###acceleration_analogue_x",
data: @vessel.acceleration[0],
flags: (
ImGui::ImGuiSliderFlags::Logarithmic
),
) do |value|
puts "x acceleration was #{@vessel.acceleration[0]}"
@vessel.acceleration[0] = value
puts "x acceleration is now #{@vessel.acceleration[0]}"
end
},
-> {
draw_slider(
label: "###acceleration_analogue_y",
data: @vessel.acceleration[1],
flags: (
ImGui::ImGuiSliderFlags::Logarithmic
),
) do |value|
Log.debug "x acceleration was #{@vessel.acceleration[1]}"
@vessel.acceleration[1] = value
Log.debug "x acceleration is now #{@vessel.acceleration[1]}"
end
},
"m/s²",
)
# automatically reset acceleration to 0, used for maneuvers
draw_table_line(
"Low thrust",
-> {
draw_slider(
label: "###acceleration_low_analogic_x",
data: @vessel.acceleration[0],
) do |value|
if !@ui_states[:acceleration_log_analogic_pressed][0]
@ui_states[:acceleration_log_analogic_save][0] = @vessel.acceleration[0]
end
@ui_states[:acceleration_log_analogic_pressed][0] = true
@vessel.acceleration[0] = value
@ui_events_handler.add(UI::EventHandler::Event::Type::MouseButtonReleased) do |callback, _|
@ui_events_handler.remove(UI::EventHandler::Event::Type::MouseButtonReleased, callback)
@ui_states[:acceleration_log_analogic_pressed][0] = false
@vessel.acceleration[0] = @ui_states[:acceleration_log_analogic_save][0]
nil
end
end
},
-> {
draw_slider(
label: "###acceleration_low_analogic_y",
data: @vessel.acceleration[1],
) do |value|
if !@ui_states[:acceleration_log_analogic_pressed][0]
@ui_states[:acceleration_log_analogic_save][0] = @vessel.acceleration[1]
end
@ui_states[:acceleration_log_analogic_pressed][0] = true
@vessel.acceleration[1] = value
lock_callback = @ui_events_handler.add(UI::EventHandler::Event::Type::KeyPressed) do |callback, event|
puts "Keypressed #{event}"
@ui_states[:acceleration_log_analogic_save][0] = value
end
@ui_events_handler.add(UI::EventHandler::Event::Type::MouseButtonReleased) do |callback, _|
@ui_events_handler.remove(UI::EventHandler::Event::Type::MouseButtonReleased, callback)
@ui_events_handler.remove(UI::EventHandler::Event::Type::MouseButtonReleased, lock_callback)
@ui_states[:acceleration_log_analogic_pressed][0] = false
@vessel.acceleration[1] = @ui_states[:acceleration_log_analogic_save][0]
nil
end
end
},
"m/s²",
)
draw_table_line(
"Computer acceleration",
-> {
draw_input(
label: "###acceleration_digit_x",
data: @ui_states[:acceleration_digit][:x],
) do |value|
@ui_states[:acceleration_digit][:x] = value
end
},
-> {
draw_input(
label: "###acceleration_digit_y",
data: @ui_states[:acceleration_digit][:y],
) do |value|
@ui_states[:acceleration_digit][:y] = value
end
},
"m/s²",
)
draw_table_line(
"",
-> {
if ImGui.button("Comfirm")
@vessel.acceleration[0] = @ui_states[:acceleration_digit][:x]
@vessel.acceleration[1] = @ui_states[:acceleration_digit][:y]
end
},
-> {
if ImGui.button("Reset")
@ui_states[:acceleration_digit][:x] = @vessel.acceleration[0]
@ui_states[:acceleration_digit][:y] = @vessel.acceleration[1]
end
},
-> {
if ImGui.button("Zero")
@ui_states[:acceleration_digit][:x] = 0.0
@ui_states[:acceleration_digit][:y] = 0.0
end
},
)
end
end
def draw_navigation(gravity_field)
# ImGui.text "Total Accele is : [#{acceleration[0].round(4)}, #{acceleration[1].round(4)}] m/s²"
ImGui.text "Pulse Accele is : [#{@vessel.acceleration[0].round(4)}, #{@vessel.acceleration[1].round(4)}] m/s²"
ImGui.text "GraviticBody is : [#{gravity_field[0].round(4)}, #{gravity_field[1].round(4)}] m/s²"
ImGui.text "Current speed is : #{@vessel.speed.magnitude.round(4)} m/s"
draw_table(title: "Actual vectors", headers: {"Name", "X", "Y", "unit"}) do
draw_table_line "Position", @vessel.position[0].round(3).to_s, @vessel.position[1].round(3).to_s, "m"
draw_table_line "Speed", @vessel.speed[0].round(3).to_s, @vessel.speed[1].round(3).to_s, "m/s"
draw_table_line "Acceleration", @vessel.acceleration[0].round(3).to_s, @vessel.acceleration[1].round(3).to_s, "m/s²"
end
end
def execute(tick : Int)
@timer.timeskip!(nanoseconds: @frequency_nano_rate)
gravity_field = @g.acceleration(@bodies[:vessel].position)
accelerate_by_tick(@bodies[:vessel])
accelerate_by_tick(@bodies[:pla1])
accelerate_by_tick(@bodies[:pla2])
# accelerate_by_tick(@bodies[:star])
if ImGui.begin("Bridge")
if ImGui.tree_node_ex("Navigation", ImGui::ImGuiTreeNodeFlags.new(ImGui::ImGuiTreeNodeFlags::DefaultOpen))
draw_navigation(gravity_field)
ImGui.tree_pop
end
if ImGui.tree_node_ex("Pilot", ImGui::ImGuiTreeNodeFlags.new(ImGui::ImGuiTreeNodeFlags::DefaultOpen))
draw_pilot
ImGui.tree_pop
end
ImGui.end
end
@window.draw(@vessel.sf_shape)
@window.draw(@star.sf_shape)
@window.draw(@pla1.sf_shape)
@window.draw(@pla2.sf_shape)
end
end
Game.new(framerate: 60, width: 800, height: 600).execute_loop

89
src/ui/event_handler.cr Normal file
View File

@ -0,0 +1,89 @@
module UI
class Event
getter :type, :code, :alt, :control, :shift, :system
@type : Type
@code : String?
@alt : String?
@control : String?
@shift : String?
@system : String?
{% if @top_level.has_constant?(:SF) %}
def initialize(event : SF::Event)
@type = SF_TO_TYPE[event.class]? || Type::Unknown
@code = event.code
@alt = event.alt
@control = event.control
@shift = event.shift
@system = event.system
end
{% end %}
enum Type
KeyPressed
KeyReleased
MouseButtonPressed
MouseButtonReleased
Unknown
end
{% if @top_level.has_constant?(:SF) %}
# can this be a named tuple instead ?
SF_TO_TYPE = {
SF::Event::KeyPressed => Type::KeyPressed,
SF::Event::KeyReleased => Type::KeyReleased,
SF::Event::MouseButtonPressed => Type::MouseButtonPressed,
SF::Event::MouseButtonReleased => Type::MouseButtonReleased,
}
{% end %}
# `Event::Handler` is able to accumulate callbacks that will be triggered every
# time a event of a given type is received.
class Handler
# A callback takes one argument (itself) and returns nothing
alias Callback = Callback, Event -> Nil
# NOTE Event.class should return Class so it's not yet what I want
def initialize
@events_callbacks = Hash(Type, Array(Callback)).new
end
def event_callbacks(event : Type) : Array(Callback)
@events_callbacks[event] ||= Array(Callback).new
@events_callbacks[event]
end
def add(event_type : Type, &block : Callback, Event -> Nil)
event_callbacks(event_type) << block
block
end
def add(event_type : Type, block : Callback)
event_callbacks(event_type) << block
block
end
def remove(event_type : Type, &block : Callback, Event -> Nil)
event_callbacks(event_type).delete(block)
end
def remove(event_type : Type, block : Callback)
event_callbacks(event_type).delete(block)
end
def handle(event : Event)
event_callbacks(event.type).each { |callback| callback.call(callback, event) }
end
{% if @top_level.has_constant?(:SF) %}
def event_callbacks(event : SF::Event.class) : Array(Callback)
event = SF_TO_TYPE[event]? || Type::Unknown
event_callbacks(event)
end
def handle(event : SF::Event)
handle(Event.new(event))
end
{% end %}
end
end
end

29
x.cr Normal file
View File

@ -0,0 +1,29 @@
class EventHandler(EventClass)
macro make_callback(event_class)
alias Callback = Callback, {{ event_class }} -> Nil
end
make_callback(EventClass)
def initialize
@s = Hash(EventClass, Array(Callback)).new
end
def add(
event,
&block : Callback, EventClass -> Nil
) : Callback, EventClass -> Nil
@s[event.class] ||= Array(Callback).new
@s[event.class] << block
block
end
end
class Event; end
class EventA < Event; end
class EventB < Event; end
h = EventHandler.new(Event)
a1 = h.add(EventA.new) { |cb, event| puts :EventA }
a2 = h.add(EventA.new) { |cb, event| puts :EventA2 }
b1 = h.add(EventB.new) { |cb, event| puts :EventB }
h.remove(a1)