|
| 1 | +require "uing" |
| 2 | +require "chipmunk" |
| 3 | + |
| 4 | +PX_PER_M = 32.0 |
| 5 | +WIN_W = 600 |
| 6 | +WIN_H = 400 |
| 7 | +DT = 1.0/120.0 |
| 8 | + |
| 9 | +def to_px(v : CP::Vect) : {Float64, Float64} |
| 10 | + {v.x * PX_PER_M, WIN_H - v.y * PX_PER_M} # Flip Y because screen Y+ is downward |
| 11 | +end |
| 12 | + |
| 13 | +# Collision types |
| 14 | +COLL_GROUND = 1 |
| 15 | +COLL_PLAYER = 2 |
| 16 | + |
| 17 | +# Main game class |
| 18 | +class Game |
| 19 | + getter space : CP::Space |
| 20 | + getter! player_body : CP::Body |
| 21 | + @player_body : CP::Body |
| 22 | + @segments = [] of {CP::Vect, CP::Vect} |
| 23 | + |
| 24 | + def initialize |
| 25 | + @space = CP::Space.new |
| 26 | + @space.gravity = CP.v(0, -9.8) |
| 27 | + @space.iterations = 10 |
| 28 | + build_terrain |
| 29 | + @player_body = build_player |
| 30 | + end |
| 31 | + |
| 32 | + # Generate gentle zigzag planks as segments |
| 33 | + def build_terrain |
| 34 | + sb = @space.static_body |
| 35 | + @segments.clear |
| 36 | + |
| 37 | + width_m = WIN_W / PX_PER_M |
| 38 | + height_m = WIN_H / PX_PER_M |
| 39 | + margin_x = 1.0 |
| 40 | + top_y = height_m - 1.2 |
| 41 | + levels = 6 |
| 42 | + len = (width_m - margin_x * 2) * 0.55 |
| 43 | + vertical_gap = (height_m - 3.0) / (levels + 1) |
| 44 | + slope_drop = 3 |
| 45 | + |
| 46 | + # Side walls and floor |
| 47 | + add_segment(sb, CP.v(margin_x * 0.2, 0.8), CP.v(margin_x * 0.2, height_m - 0.5)) |
| 48 | + add_segment(sb, CP.v(width_m - margin_x * 0.2, 0.8), CP.v(width_m - margin_x * 0.2, height_m - 0.5)) |
| 49 | + add_segment(sb, CP.v(margin_x * 0.2, 0.8), CP.v(width_m - margin_x * 0.2, 0.8)) |
| 50 | + |
| 51 | + levels.times do |i| |
| 52 | + y = top_y - i * vertical_gap |
| 53 | + if i.even? |
| 54 | + x0 = margin_x |
| 55 | + x1 = (margin_x + len).clamp(margin_x, width_m - margin_x) |
| 56 | + y0 = y |
| 57 | + y1 = (y - slope_drop).clamp(0.8, height_m - 0.5) |
| 58 | + else |
| 59 | + x1 = width_m - margin_x |
| 60 | + x0 = (x1 - len).clamp(margin_x, width_m - margin_x) |
| 61 | + y1 = y |
| 62 | + y0 = (y - slope_drop).clamp(0.8, height_m - 0.5) |
| 63 | + end |
| 64 | + add_segment(sb, CP.v(x0, y0), CP.v(x1, y1)) |
| 65 | + end |
| 66 | + end |
| 67 | + |
| 68 | + # Player is a single circle (simple model) |
| 69 | + def build_player : CP::Body |
| 70 | + mass = 70.0 |
| 71 | + radius = 0.30 |
| 72 | + moment = CP::Shape::Circle.moment(mass, 0.0, radius, CP.v(0, 0)) |
| 73 | + body = CP::Body.new(mass, moment) |
| 74 | + # Start above the first plank on the left |
| 75 | + start_x = 1.2 |
| 76 | + start_y = (WIN_H / PX_PER_M) - 1.0 |
| 77 | + body.position = CP.v(start_x, start_y) |
| 78 | + @space.add body |
| 79 | + shape = CP::Shape::Circle.new(body, radius, CP.v(0, 0)) |
| 80 | + shape.friction = 0.95 |
| 81 | + shape.elasticity = 1.0 |
| 82 | + shape.collision_type = COLL_PLAYER |
| 83 | + @space.add shape |
| 84 | + body |
| 85 | + end |
| 86 | + |
| 87 | + # Restart (R key) |
| 88 | + def reset! |
| 89 | + # Recreate space for a clean state |
| 90 | + @space = CP::Space.new |
| 91 | + @space.gravity = CP.v(0, -9.8) |
| 92 | + @space.iterations = 10 |
| 93 | + build_terrain |
| 94 | + @player_body = build_player |
| 95 | + end |
| 96 | + |
| 97 | + # Step physics (twice per frame for ~60FPS) |
| 98 | + def update |
| 99 | + 2.times { @space.step(DT) } |
| 100 | + end |
| 101 | + |
| 102 | + # Segments for drawing |
| 103 | + def terrain_segments : Array({CP::Vect, CP::Vect}) |
| 104 | + @segments |
| 105 | + end |
| 106 | + |
| 107 | + private def add_segment(sb : CP::Body, a : CP::Vect, b : CP::Vect) |
| 108 | + seg = CP::Shape::Segment.new(sb, a, b, 0.05) |
| 109 | + seg.friction = 1.1 |
| 110 | + seg.elasticity = 0.6 |
| 111 | + seg.collision_type = COLL_GROUND |
| 112 | + @space.add seg |
| 113 | + @segments << {a, b} |
| 114 | + end |
| 115 | +end |
| 116 | + |
| 117 | +UIng.init |
| 118 | +game = Game.new |
| 119 | + |
| 120 | +window = UIng::Window.new("ZigZag (Chipmunk)", WIN_W, WIN_H, menubar: false) |
| 121 | +box = UIng::Box.new(:vertical) |
| 122 | +label = UIng::Label.new("Press [R] to reset") |
| 123 | + |
| 124 | +# Area handler: drawing and key input |
| 125 | +handler = UIng::Area::Handler.new do |
| 126 | + draw do |area, params| |
| 127 | + ctx = params.context |
| 128 | + bg = UIng::Area::Draw::Brush.new(:solid, 0.96, 0.98, 1.0, 1.0) |
| 129 | + ctx.fill_path(bg) { |p| p.add_rectangle(0, 0, WIN_W, WIN_H) } |
| 130 | + line = UIng::Area::Draw::Brush.new(:solid, 0.0, 0.0, 0.0, 1.0) |
| 131 | + game.terrain_segments.each do |(a, b)| |
| 132 | + x0, y0 = to_px(a) |
| 133 | + x1, y1 = to_px(b) |
| 134 | + ctx.stroke_path(line, thickness: 3.0) do |path| |
| 135 | + path.new_figure(x0, y0) |
| 136 | + path.line_to(x1, y1) |
| 137 | + end |
| 138 | + end |
| 139 | + pos = game.player_body.position |
| 140 | + x, y = to_px(pos) |
| 141 | + player = UIng::Area::Draw::Brush.new(:solid, 0.2, 0.2, 0.2, 1.0) |
| 142 | + ctx.fill_path(player) do |path| |
| 143 | + path.new_figure_with_arc(x, y, 0.30 * PX_PER_M, 0, Math::PI * 2, false) |
| 144 | + end |
| 145 | + label.text = "Press [R] to reset" |
| 146 | + end |
| 147 | + key_event do |area, event| |
| 148 | + # Ignore key repeat (only handle key down) |
| 149 | + next true if event.up != 0 |
| 150 | + case event.key |
| 151 | + when 'r', 'R' |
| 152 | + game.reset! |
| 153 | + area.queue_redraw_all |
| 154 | + true |
| 155 | + else |
| 156 | + false |
| 157 | + end |
| 158 | + end |
| 159 | +end |
| 160 | +area = UIng::Area.new(handler, WIN_W, WIN_H - 40) |
| 161 | + |
| 162 | +box.append(area, stretchy: true) |
| 163 | +box.append(label, stretchy: false) |
| 164 | +window.child = box |
| 165 | + |
| 166 | +UIng.timer(8) do |
| 167 | + game.update |
| 168 | + area.queue_redraw_all |
| 169 | + 1 |
| 170 | +end |
| 171 | + |
| 172 | +window.on_closing { UIng.quit; true } |
| 173 | +window.show |
| 174 | +UIng.main |
0 commit comments