bouncy fpga game
www.youtube.com/watch?v=IiLWF3GbV7w
1-- ========================================================================
2-- Physics Engine (scrolling 1500x1500 world)
3-- Positions: 11-bit unsigned. Valid range: 0..1492 (fits in 11-bit fine).
4-- Velocities: 10-bit 2's complement, bit 9 = sign.
5--
6-- Underflow detection: uses >= 2000 threshold instead of bit-10 sign check.
7-- Bit-10 was wrong because GROUND(1480) and RIGHT_WALL(1492) both have
8-- bit 10 set (values >= 1024), causing false ceiling/wall triggers.
9--
10-- Obstacles imported from level package (use work.level.all).
11-- ========================================================================
12
13library IEEE;
14use IEEE.STD_LOGIC_1164.all;
15use IEEE.STD_LOGIC_ARITH.all;
16use IEEE.STD_LOGIC_UNSIGNED.all;
17use work.level.all;
18
19entity physics_engine is
20 port(
21 vert_sync : in std_logic;
22 key_w : in std_logic;
23 key_a : in std_logic;
24 key_s : in std_logic;
25 key_d : in std_logic;
26 char_x : out std_logic_vector(10 downto 0);
27 char_y : out std_logic_vector(10 downto 0);
28 char_width : out std_logic_vector(9 downto 0);
29 char_height : out std_logic_vector(9 downto 0);
30 cam_x : out std_logic_vector(10 downto 0);
31 cam_y : out std_logic_vector(10 downto 0);
32 vel_out : out std_logic_vector(9 downto 0);
33 vel_x_out : out std_logic_vector(9 downto 0);
34 vel_y_out : out std_logic_vector(9 downto 0)
35 );
36end physics_engine;
37
38architecture behavior of physics_engine is
39
40 -- Character state
41 signal pos_x : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(100, 11);
42 signal pos_y : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1400, 11);
43 signal vel_x : std_logic_vector(9 downto 0) := (others => '0');
44 signal vel_y : std_logic_vector(9 downto 0) := (others => '0');
45 signal cam_x_sig : std_logic_vector(10 downto 0) := (others => '0');
46 signal cam_y_sig : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1020, 11);
47
48 -- SIZE: 10-bit for squish output math, 11-bit for position arithmetic
49 constant SIZE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(7, 10);
50 constant SIZE11 : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(7, 11);
51
52 -- Physics tuning
53 constant GRAVITY : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(1, 10);
54 constant IMPULSE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(3, 10);
55 constant SLAM_TAP_FORCE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(18, 10);
56 constant SLAM_BOOST_CLOSE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(24, 10);
57 constant SLAM_BOOST_MED : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(16, 10);
58 constant SLAM_BOOST_FAR : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(8, 10);
59 constant SLAM_CLOSE_THR : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(270, 11);
60 constant SLAM_MED_THR : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(660, 11);
61 -- Initial jump: full force at zero velocity, tapers off as |vel_y| rises.
62 -- Piecewise lookup approximates: max(JUMP_MIN, JUMP_BASE*JUMP_SCALE/(JUMP_SCALE+|vy|))
63 constant JUMP_BASE : integer := 14; -- force at zero velocity
64 constant JUMP_VEL_THR1 : integer := 3; -- |vy| <= 3 → force 14
65 constant JUMP_VEL_THR2 : integer := 10; -- |vy| <= 10 → force 8
66 constant JUMP_VEL_THR3 : integer := 20; -- |vy| <= 20 → force 6
67 constant JUMP_MIN_FORCE : integer := 4; -- floor at high velocity
68 -- Trampoline: fixed additive boost on each ground contact while W held.
69 -- No inverse scaling so energy accumulates each bounce.
70 constant JUMP_BOUNCE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(6, 10);
71 constant MAX_VEL_X : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(32, 10);
72
73 -- World bounds (11-bit)
74 constant GROUND : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1480, 11);
75 constant CEILING : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(16, 11);
76 constant LEFT_WALL : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(8, 11);
77 constant RIGHT_WALL : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1492, 11);
78
79 -- Underflow sentinel: valid positions top out at 1480 (GROUND).
80 -- CEILING(16) - max_upward(128) = -112 -> 11-bit unsigned = 1936.
81 -- So UNDERFLOW must be <= 1936 and > 1480. 1500 gives safe margin.
82 constant UNDERFLOW : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1500, 11);
83
84 -- Tune: vertical speed (0-63) at which all 10 LEDs are fully lit.
85 -- Lower = more sensitive (fewer LEDs at low speed fill up faster).
86 -- Higher = better dynamic range across the full velocity range.
87 -- Higher = less sensitive (need a harder bounce to light all LEDs).
88 constant LED_FULL_VEL : integer := 48;
89
90 -- Animation
91 signal squish : std_logic_vector(3 downto 0) := (others => '0');
92 signal squish_h : std_logic := '0';
93 signal on_ground : std_logic := '0';
94 signal jump_pressed : std_logic := '0';
95 signal slam_held : std_logic := '0';
96 signal slam_tap : std_logic := '0'; -- S pressed then released before landing
97 signal slam_start_y : std_logic_vector(10 downto 0) := (others => '0');
98 signal prev_key_s : std_logic := '0';
99
100 signal abs_vel_x : std_logic_vector(9 downto 0);
101 signal abs_vel_y : std_logic_vector(9 downto 0);
102
103begin
104
105 char_x <= pos_x;
106 char_y <= pos_y;
107 cam_x <= cam_x_sig;
108 cam_y <= cam_y_sig;
109 vel_x_out <= vel_x;
110 vel_y_out <= vel_y;
111
112 -- Absolute values of both velocity axes.
113 abs_vel_x <= (not vel_x) + 1 when vel_x(9) = '1' else vel_x;
114 abs_vel_y <= (not vel_y) + 1 when vel_y(9) = '1' else vel_y;
115
116 -- Thermometer-encode peak speed (max of |vx|,|vy|) → horizontal LED bar.
117 -- n LEDs lit from LSB up → 0000011111 for n=5, 1111111111 for n=10.
118 vel_bar : process(abs_vel_x, abs_vel_y)
119 variable vx, vy, vmax : integer;
120 variable n : integer range 0 to 10;
121 begin
122 vx := CONV_INTEGER(abs_vel_x);
123 vy := CONV_INTEGER(abs_vel_y);
124 if vx > vy then vmax := vx; else vmax := vy; end if;
125 if vmax >= LED_FULL_VEL then n := 10;
126 else n := vmax * 10 / LED_FULL_VEL;
127 end if;
128 case n is
129 when 0 => vel_out <= "0000000000";
130 when 1 => vel_out <= "0000000001";
131 when 2 => vel_out <= "0000000011";
132 when 3 => vel_out <= "0000000111";
133 when 4 => vel_out <= "0000001111";
134 when 5 => vel_out <= "0000011111";
135 when 6 => vel_out <= "0000111111";
136 when 7 => vel_out <= "0001111111";
137 when 8 => vel_out <= "0011111111";
138 when 9 => vel_out <= "0111111111";
139 when others => vel_out <= "1111111111";
140 end case;
141 end process vel_bar;
142
143 char_width <= SIZE + ("000000" & squish) when squish_h = '0'
144 else SIZE - ("000000" & squish(3 downto 1));
145 char_height <= SIZE - ("000000" & squish(3 downto 1)) when squish_h = '0'
146 else SIZE + ("000000" & squish);
147
148 physics : process
149 variable vx, vy : std_logic_vector(9 downto 0);
150 variable px, py : std_logic_vector(10 downto 0);
151 variable bounced : std_logic;
152 variable bounce_wall : std_logic;
153 variable bounce_speed : std_logic_vector(9 downto 0);
154 variable grounded : std_logic;
155 variable c_left, c_right, c_top, c_bot : std_logic_vector(10 downto 0);
156 variable c_prev_top, c_prev_bot, c_prev_left, c_prev_right : std_logic_vector(10 downto 0);
157 variable tcx, tcy, dcx, dcy : std_logic_vector(10 downto 0);
158 variable abs_vy_int : integer;
159 variable jforce_int : integer;
160 variable jforce_slv : std_logic_vector(9 downto 0);
161 variable slam_dist : std_logic_vector(10 downto 0);
162 variable slam_boost : std_logic_vector(9 downto 0);
163 variable x_overlap : boolean;
164 variable y_overlap : boolean;
165 begin
166 wait until vert_sync'event and vert_sync = '1';
167
168 vx := vel_x; vy := vel_y;
169 bounced := '0'; bounce_wall := '0';
170 bounce_speed := (others => '0');
171 grounded := '0';
172
173 -- == INPUT ==
174 if key_w = '0' then jump_pressed <= '0'; end if;
175
176 -- Initial jump: force tapers as |vy| increases (inverse scaling)
177 if vy(9) = '1' then abs_vy_int := CONV_INTEGER((not vy) + 1);
178 else abs_vy_int := CONV_INTEGER(vy); end if;
179 if abs_vy_int <= JUMP_VEL_THR1 then jforce_int := JUMP_BASE;
180 elsif abs_vy_int <= JUMP_VEL_THR2 then jforce_int := 8;
181 elsif abs_vy_int <= JUMP_VEL_THR3 then jforce_int := 6;
182 else jforce_int := JUMP_MIN_FORCE;
183 end if;
184 jforce_slv := CONV_STD_LOGIC_VECTOR(jforce_int, 10);
185
186 if key_w = '1' and jump_pressed = '0' and on_ground = '1' then
187 vy := vy - jforce_slv;
188 jump_pressed <= '1';
189 end if;
190
191 -- Trampoline: fixed boost (no inverse scaling) so energy accumulates each bounce
192 if key_w = '1' and jump_pressed = '1' and on_ground = '1' then
193 vy := vy - JUMP_BOUNCE;
194 end if;
195
196 -- S slam: rising edge in air = tap burst downward; hold to get landing boost
197 if key_s = '1' and prev_key_s = '0' and on_ground = '0' then
198 slam_start_y <= pos_y;
199 slam_held <= '1';
200 slam_tap <= '1';
201 vy := vy + SLAM_TAP_FORCE;
202 end if;
203 -- S released before landing: mark as tap, clear hold
204 if key_s = '0' and slam_held = '1' then slam_held <= '0'; end if;
205 prev_key_s <= key_s;
206
207 if key_a = '1' then
208 if on_ground = '1' then vx := vx - IMPULSE; else vx := vx - 2; end if;
209 end if;
210 if key_d = '1' then
211 if on_ground = '1' then vx := vx + IMPULSE; else vx := vx + 2; end if;
212 end if;
213
214 -- == GRAVITY ==
215 vy := vy + GRAVITY;
216
217 -- == FRICTION: vel -= vel/4, min 1 ==
218 if vx(9) = '0' then
219 if vx > 0 then
220 if vx(9 downto 2) = "00000000" then vx := vx - 1;
221 else vx := vx - ("00" & vx(9 downto 2)); end if;
222 end if;
223 else
224 if vx /= "0000000000" then
225 if vx(9 downto 2) = "11111111" then vx := vx + 1;
226 else vx := vx - ("11" & vx(9 downto 2)); end if;
227 end if;
228 end if;
229
230 -- == CLAMP velocities ==
231 if vx(9) = '0' and vx > MAX_VEL_X then vx := MAX_VEL_X; end if;
232 if vx(9) = '1' and vx < (not MAX_VEL_X) + 1 then vx := (not MAX_VEL_X) + 1; end if;
233 if vy(9) = '0' and vy > 80 then vy := CONV_STD_LOGIC_VECTOR(80, 10); end if;
234 if vy(9) = '1' and vy < CONV_STD_LOGIC_VECTOR(924, 10) then -- 924 = -100
235 vy := CONV_STD_LOGIC_VECTOR(924, 10);
236 end if;
237
238 -- == MOVE: sign-extend 10-bit velocity to 11-bit before adding ==
239 px := pos_x + (vx(9) & vx);
240 py := pos_y + (vy(9) & vy);
241
242 -- == GROUND ==
243 if py >= GROUND then
244 py := GROUND;
245 bounced := '1'; grounded := '1';
246 bounce_speed := vy;
247 vy := (not vy) + 1;
248 if vy(9) = '1' then
249 vy := vy + ("000" & ((not vy(9 downto 3)) + 1));
250 end if;
251 if vy(9) = '1' and vy >= CONV_STD_LOGIC_VECTOR(1022, 10) then
252 vy := (others => '0');
253 elsif vy(9) = '0' then vy := (others => '0'); end if;
254 end if;
255
256 -- == CEILING ==
257 if py >= UNDERFLOW or py <= CEILING then
258 py := CEILING;
259 bounced := '1';
260 bounce_speed := (not vy) + 1;
261 vy := (not vy) + 1;
262 if vy(9) = '0' and vy > 1 then
263 vy := vy - ("000" & vy(9 downto 3));
264 end if;
265 end if;
266
267 -- == LEFT WALL ==
268 if px >= UNDERFLOW or px <= LEFT_WALL + SIZE11 then
269 px := LEFT_WALL + SIZE11;
270 bounced := '1'; bounce_wall := '1';
271 bounce_speed := (not vx) + 1;
272 vx := (not vx) + 1;
273 if vx(9) = '0' and vx > 1 then
274 vx := vx - ("000" & vx(9 downto 3));
275 end if;
276 end if;
277
278 -- == RIGHT WALL ==
279 if px >= RIGHT_WALL - SIZE11 then
280 px := RIGHT_WALL - SIZE11;
281 bounced := '1'; bounce_wall := '1';
282 bounce_speed := vx;
283 vx := (not vx) + 1;
284 if vx(9) = '1' then
285 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
286 end if;
287 if vx(9) = '1' and vx >= CONV_STD_LOGIC_VECTOR(1022, 10) then
288 vx := (others => '0');
289 end if;
290 end if;
291
292 -- == OBSTACLE COLLISIONS (swept AABB) ==
293 c_prev_top := pos_y - SIZE11;
294 c_prev_bot := pos_y + SIZE11;
295 c_prev_left := pos_x - SIZE11;
296 c_prev_right := pos_x + SIZE11;
297
298 for obs_i in 0 to OBS_COUNT-1 loop
299 c_left := px - SIZE11;
300 c_right := px + SIZE11;
301 c_top := py - SIZE11;
302 c_bot := py + SIZE11;
303
304 -- Swept overlap: strict on "was on correct side" to prevent wall sticking
305 if vx(9) = '0' then
306 x_overlap := (c_right >= OBS_L(obs_i)) and (c_prev_left < OBS_R(obs_i));
307 else
308 x_overlap := (c_prev_right > OBS_L(obs_i)) and (c_left <= OBS_R(obs_i));
309 end if;
310
311 -- Strict y: exclude characters at/below bottom face (down) or at/above top face (up)
312 if vy(9) = '0' then
313 y_overlap := (c_bot >= OBS_T(obs_i)) and (c_prev_top < OBS_B(obs_i));
314 else
315 y_overlap := (c_prev_bot > OBS_T(obs_i)) and (c_top <= OBS_B(obs_i));
316 end if;
317
318 if x_overlap and y_overlap then
319
320 if c_prev_bot <= OBS_T(obs_i) + SIZE11 and vy(9) = '0' then
321 py := OBS_T(obs_i) - SIZE11; grounded := '1';
322 bounced := '1';
323 vy := (not vy) + 1;
324 if vy(9) = '0' and vy > 1 then
325 vy := vy - ("000" & vy(9 downto 3));
326 elsif vy(9) = '1' and vy < CONV_STD_LOGIC_VECTOR(1022, 10) then
327 vy := vy + ("000" & ((not vy(9 downto 3)) + 1));
328 end if;
329 if vy(9) = '0' and vy < 2 then vy := (others => '0'); end if;
330 if vy(9) = '1' and vy >= CONV_STD_LOGIC_VECTOR(1022, 10) then
331 vy := (others => '0');
332 end if;
333
334 elsif c_prev_top >= OBS_B(obs_i) and vy(9) = '1' then
335 py := OBS_B(obs_i) + SIZE11;
336 bounced := '1';
337 vy := (not vy) + 1;
338 if vy(9) = '0' and vy > 1 then
339 vy := vy - ("000" & vy(9 downto 3));
340 end if;
341
342 elsif c_prev_right <= OBS_L(obs_i) and vx(9) = '0' then
343 px := OBS_L(obs_i) - SIZE11;
344 bounced := '1'; bounce_wall := '1';
345 vx := (not vx) + 1;
346 if vx(9) = '0' and vx > 1 then
347 vx := vx - ("000" & vx(9 downto 3));
348 elsif vx(9) = '1' and vx < CONV_STD_LOGIC_VECTOR(1022, 10) then
349 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
350 end if;
351
352 elsif c_prev_left >= OBS_R(obs_i) and vx(9) = '1' then
353 px := OBS_R(obs_i) + SIZE11;
354 bounced := '1'; bounce_wall := '1';
355 vx := (not vx) + 1;
356 if vx(9) = '0' and vx > 1 then
357 vx := vx - ("000" & vx(9 downto 3));
358 elsif vx(9) = '1' and vx < CONV_STD_LOGIC_VECTOR(1022, 10) then
359 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
360 end if;
361
362 else
363 if vy(9) = '0' then py := OBS_T(obs_i) - SIZE11; grounded := '1';
364 else py := OBS_B(obs_i) + SIZE11; end if;
365 bounced := '1';
366 vy := (not vy) + 1;
367 if vy(9) = '0' and vy > 1 then
368 vy := vy - ("000" & vy(9 downto 3));
369 end if;
370 end if;
371 end if;
372 end loop;
373
374 -- == SLAM LANDING RESOLUTION ==
375 if grounded = '1' and slam_tap = '1' then
376 if slam_held = '0' then
377 -- Tap slam: S released before landing — kill bounce, plant on surface
378 vy := (others => '0');
379 else
380 -- Hold slam: distance-based upward boost
381 slam_dist := py - slam_start_y;
382 if slam_dist < SLAM_CLOSE_THR then
383 slam_boost := SLAM_BOOST_CLOSE;
384 elsif slam_dist < SLAM_MED_THR then
385 slam_boost := SLAM_BOOST_MED;
386 else
387 slam_boost := SLAM_BOOST_FAR;
388 end if;
389 vy := vy - slam_boost;
390 slam_held <= '0';
391 end if;
392 slam_tap <= '0';
393 end if;
394
395 -- == COMMIT ==
396 vel_x <= vx;
397 vel_y <= vy;
398 pos_x <= px;
399 pos_y <= py;
400
401 -- == CAMERA: lag-follow character (1/8 of gap per frame, min 1px) ==
402 -- Compute clamped target
403 if px < CONV_STD_LOGIC_VECTOR(320, 11) then
404 tcx := (others => '0');
405 elsif px > CONV_STD_LOGIC_VECTOR(1180, 11) then
406 tcx := CONV_STD_LOGIC_VECTOR(860, 11);
407 else
408 tcx := px - CONV_STD_LOGIC_VECTOR(320, 11);
409 end if;
410
411 if py < CONV_STD_LOGIC_VECTOR(240, 11) then
412 tcy := (others => '0');
413 elsif py > CONV_STD_LOGIC_VECTOR(1260, 11) then
414 tcy := CONV_STD_LOGIC_VECTOR(1020, 11);
415 else
416 tcy := py - CONV_STD_LOGIC_VECTOR(240, 11);
417 end if;
418
419 -- Slide camera toward target
420 if cam_x_sig < tcx then
421 dcx := tcx - cam_x_sig;
422 if dcx(10 downto 3) = "00000000" then
423 cam_x_sig <= cam_x_sig + 1;
424 else
425 cam_x_sig <= cam_x_sig + ("000" & dcx(10 downto 3));
426 end if;
427 elsif cam_x_sig > tcx then
428 dcx := cam_x_sig - tcx;
429 if dcx(10 downto 3) = "00000000" then
430 cam_x_sig <= cam_x_sig - 1;
431 else
432 cam_x_sig <= cam_x_sig - ("000" & dcx(10 downto 3));
433 end if;
434 end if;
435
436 if cam_y_sig < tcy then
437 dcy := tcy - cam_y_sig;
438 if dcy(10 downto 3) = "00000000" then
439 cam_y_sig <= cam_y_sig + 1;
440 else
441 cam_y_sig <= cam_y_sig + ("000" & dcy(10 downto 3));
442 end if;
443 elsif cam_y_sig > tcy then
444 dcy := cam_y_sig - tcy;
445 if dcy(10 downto 3) = "00000000" then
446 cam_y_sig <= cam_y_sig - 1;
447 else
448 cam_y_sig <= cam_y_sig - ("000" & dcy(10 downto 3));
449 end if;
450 end if;
451
452 -- == SQUISH ==
453 if bounced = '1' and bounce_speed > 3 then
454 if bounce_speed >= 8 then squish <= "1000";
455 else squish <= bounce_speed(3 downto 0); end if;
456 squish_h <= bounce_wall;
457 elsif squish > 0 then
458 squish <= squish - 1;
459 end if;
460
461 if grounded = '1' then on_ground <= '1';
462 else on_ground <= '0'; end if;
463
464 end process physics;
465
466end behavior;