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
|
/zero_epsilon
|
||||||
/lib
|
/lib
|
||||||
|
/imgui.ini
|
||||||
|
/docs
|
||||||
|
/physics_sandbox
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -13,7 +13,7 @@ debug:
|
||||||
release:
|
release:
|
||||||
crystal build src/$(NAME).cr --stats --release
|
crystal build src/$(NAME).cr --stats --release
|
||||||
test:
|
test:
|
||||||
crystal spec
|
crystal spec --error-trace
|
||||||
deps:
|
deps:
|
||||||
shards install
|
shards install
|
||||||
deps_update:
|
deps_update:
|
||||||
|
@ -21,7 +21,7 @@ deps_update:
|
||||||
deps_opt:
|
deps_opt:
|
||||||
@[ -d lib/ ] || make deps
|
@[ -d lib/ ] || make deps
|
||||||
doc:
|
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:
|
clean:
|
||||||
rm $(NAME)
|
rm $(NAME)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,9 @@ After you build the game, run it with
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
You can send email or join me on irc.
|
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>.
|
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.
|
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