Compare commits
25 Commits
master
...
event-as-g
Author | SHA1 | Date | |
---|---|---|---|
3469024b23 | |||
8012ff40af | |||
c48d350f58 | |||
01cc102d88 | |||
c4012f2400 | |||
873703e7ce | |||
81b00b576b | |||
3a47b165f2 | |||
b2f98faa93 | |||
8a9845807e | |||
3f93a7729b | |||
56b4b139f1 | |||
10cc4e3b96 | |||
70af8da4a4 | |||
7b7501d61c | |||
2216222a8b | |||
35db808f20 | |||
ec4348b652 | |||
30f4712da8 | |||
09be5680a4 | |||
fb6a8a4df4 | |||
978eb648e2 | |||
ad8d62556e | |||
7505fcfac7 | |||
2d999847b7 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
|||
/zero_epsilon
|
||||
/lib
|
||||
/imgui.ini
|
||||
/docs
|
||||
/physics_sandbox
|
||||
|
|
4
Makefile
4
Makefile
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
14
shard.lock
Normal 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
|
||||
|
15
spec/engine/gravity/body_spec.cr
Normal file
15
spec/engine/gravity/body_spec.cr
Normal 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
|
12
spec/engine/gravity/field_spec.cr
Normal file
12
spec/engine/gravity/field_spec.cr
Normal 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
|
14
spec/engine/gravity/moving_body_spec.cr
Normal file
14
spec/engine/gravity/moving_body_spec.cr
Normal 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
111
spec/engine/physics_spec.cr
Normal 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
140
spec/engine/vector_spec.cr
Normal 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
|
23
spec/frequency_engine_spec.cr
Normal file
23
spec/frequency_engine_spec.cr
Normal 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
|
79
spec/ui/event_handler_spec.cr
Normal file
79
spec/ui/event_handler_spec.cr
Normal 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
1
src/engine/gravity.cr
Normal file
|
@ -0,0 +1 @@
|
|||
require "./gravity/*"
|
40
src/engine/gravity/body.cr
Normal file
40
src/engine/gravity/body.cr
Normal 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
|
39
src/engine/gravity/field.cr
Normal file
39
src/engine/gravity/field.cr
Normal 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
|
10
src/engine/gravity/moving_body.cr
Normal file
10
src/engine/gravity/moving_body.cr
Normal 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
53
src/engine/physics.cr
Normal 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
167
src/engine/vector.cr
Normal 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
24
src/frequency_loop.cr
Normal 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
102
src/imgui_helper.cr
Normal 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
|
348
src/tests/physics_sandbox.cr
Normal file
348
src/tests/physics_sandbox.cr
Normal 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
89
src/ui/event_handler.cr
Normal 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
29
x.cr
Normal 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)
|
Loading…
Reference in New Issue
Block a user