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_start_y : std_logic_vector(10 downto 0) := (others => '0');
97 signal prev_key_s : std_logic := '0';
98
99 signal abs_vel_x : std_logic_vector(9 downto 0);
100 signal abs_vel_y : std_logic_vector(9 downto 0);
101
102begin
103
104 char_x <= pos_x;
105 char_y <= pos_y;
106 cam_x <= cam_x_sig;
107 cam_y <= cam_y_sig;
108 vel_x_out <= vel_x;
109 vel_y_out <= vel_y;
110
111 -- Absolute values of both velocity axes.
112 abs_vel_x <= (not vel_x) + 1 when vel_x(9) = '1' else vel_x;
113 abs_vel_y <= (not vel_y) + 1 when vel_y(9) = '1' else vel_y;
114
115 -- Thermometer-encode peak speed (max of |vx|,|vy|) → horizontal LED bar.
116 -- n LEDs lit from LSB up → 0000011111 for n=5, 1111111111 for n=10.
117 vel_bar : process(abs_vel_x, abs_vel_y)
118 variable vx, vy, vmax : integer;
119 variable n : integer range 0 to 10;
120 begin
121 vx := CONV_INTEGER(abs_vel_x);
122 vy := CONV_INTEGER(abs_vel_y);
123 if vx > vy then vmax := vx; else vmax := vy; end if;
124 if vmax >= LED_FULL_VEL then n := 10;
125 else n := vmax * 10 / LED_FULL_VEL;
126 end if;
127 case n is
128 when 0 => vel_out <= "0000000000";
129 when 1 => vel_out <= "0000000001";
130 when 2 => vel_out <= "0000000011";
131 when 3 => vel_out <= "0000000111";
132 when 4 => vel_out <= "0000001111";
133 when 5 => vel_out <= "0000011111";
134 when 6 => vel_out <= "0000111111";
135 when 7 => vel_out <= "0001111111";
136 when 8 => vel_out <= "0011111111";
137 when 9 => vel_out <= "0111111111";
138 when others => vel_out <= "1111111111";
139 end case;
140 end process vel_bar;
141
142 char_width <= SIZE + ("000000" & squish) when squish_h = '0'
143 else SIZE - ("000000" & squish(3 downto 1));
144 char_height <= SIZE - ("000000" & squish(3 downto 1)) when squish_h = '0'
145 else SIZE + ("000000" & squish);
146
147 physics : process
148 variable vx, vy : std_logic_vector(9 downto 0);
149 variable px, py : std_logic_vector(10 downto 0);
150 variable bounced : std_logic;
151 variable bounce_wall : std_logic;
152 variable bounce_speed : std_logic_vector(9 downto 0);
153 variable grounded : std_logic;
154 variable c_left, c_right, c_top, c_bot : std_logic_vector(10 downto 0);
155 variable c_prev_top, c_prev_bot, c_prev_left, c_prev_right : std_logic_vector(10 downto 0);
156 variable tcx, tcy, dcx, dcy : std_logic_vector(10 downto 0);
157 variable abs_vy_int : integer;
158 variable jforce_int : integer;
159 variable jforce_slv : std_logic_vector(9 downto 0);
160 variable slam_dist : std_logic_vector(10 downto 0);
161 variable slam_boost : std_logic_vector(9 downto 0);
162 begin
163 wait until vert_sync'event and vert_sync = '1';
164
165 vx := vel_x; vy := vel_y;
166 bounced := '0'; bounce_wall := '0';
167 bounce_speed := (others => '0');
168 grounded := '0';
169
170 -- == INPUT ==
171 if key_w = '0' then jump_pressed <= '0'; end if;
172
173 -- Initial jump: force tapers as |vy| increases (inverse scaling)
174 if vy(9) = '1' then abs_vy_int := CONV_INTEGER((not vy) + 1);
175 else abs_vy_int := CONV_INTEGER(vy); end if;
176 if abs_vy_int <= JUMP_VEL_THR1 then jforce_int := JUMP_BASE;
177 elsif abs_vy_int <= JUMP_VEL_THR2 then jforce_int := 8;
178 elsif abs_vy_int <= JUMP_VEL_THR3 then jforce_int := 6;
179 else jforce_int := JUMP_MIN_FORCE;
180 end if;
181 jforce_slv := CONV_STD_LOGIC_VECTOR(jforce_int, 10);
182
183 if key_w = '1' and jump_pressed = '0' and on_ground = '1' then
184 vy := vy - jforce_slv;
185 jump_pressed <= '1';
186 end if;
187
188 -- Trampoline: fixed boost (no inverse scaling) so energy accumulates each bounce
189 if key_w = '1' and jump_pressed = '1' and on_ground = '1' then
190 vy := vy - JUMP_BOUNCE;
191 end if;
192
193 -- S slam: rising edge in air = tap burst downward; hold to get landing boost
194 if key_s = '1' and prev_key_s = '0' and on_ground = '0' then
195 slam_start_y <= pos_y;
196 slam_held <= '1';
197 vy := vy + SLAM_TAP_FORCE;
198 end if;
199 if key_s = '0' then slam_held <= '0'; end if;
200 prev_key_s <= key_s;
201
202 if key_a = '1' then
203 if on_ground = '1' then vx := vx - IMPULSE; else vx := vx - 2; end if;
204 end if;
205 if key_d = '1' then
206 if on_ground = '1' then vx := vx + IMPULSE; else vx := vx + 2; end if;
207 end if;
208
209 -- == GRAVITY ==
210 vy := vy + GRAVITY;
211
212 -- == FRICTION: vel -= vel/4, min 1 ==
213 if vx(9) = '0' then
214 if vx > 0 then
215 if vx(9 downto 2) = "00000000" then vx := vx - 1;
216 else vx := vx - ("00" & vx(9 downto 2)); end if;
217 end if;
218 else
219 if vx /= "0000000000" then
220 if vx(9 downto 2) = "11111111" then vx := vx + 1;
221 else vx := vx - ("11" & vx(9 downto 2)); end if;
222 end if;
223 end if;
224
225 -- == CLAMP velocities ==
226 if vx(9) = '0' and vx > MAX_VEL_X then vx := MAX_VEL_X; end if;
227 if vx(9) = '1' and vx < (not MAX_VEL_X) + 1 then vx := (not MAX_VEL_X) + 1; end if;
228 if vy(9) = '0' and vy > 80 then vy := CONV_STD_LOGIC_VECTOR(80, 10); end if;
229 if vy(9) = '1' and vy < CONV_STD_LOGIC_VECTOR(924, 10) then -- 924 = -100
230 vy := CONV_STD_LOGIC_VECTOR(924, 10);
231 end if;
232
233 -- == MOVE: sign-extend 10-bit velocity to 11-bit before adding ==
234 px := pos_x + (vx(9) & vx);
235 py := pos_y + (vy(9) & vy);
236
237 -- == GROUND ==
238 if py >= GROUND then
239 py := GROUND;
240 bounced := '1'; grounded := '1';
241 bounce_speed := vy;
242 vy := (not vy) + 1;
243 if vy(9) = '1' then
244 vy := vy + ("000" & ((not vy(9 downto 3)) + 1));
245 end if;
246 if vy(9) = '1' and vy >= CONV_STD_LOGIC_VECTOR(1022, 10) then
247 vy := (others => '0');
248 elsif vy(9) = '0' then vy := (others => '0'); end if;
249 end if;
250
251 -- == CEILING ==
252 -- Underflow detection: py >= 2000 means it wrapped below zero.
253 -- (GROUND=1480, RIGHT_WALL=1492 are both < 2000, so no false triggers.)
254 if py >= UNDERFLOW or py <= CEILING then
255 py := CEILING;
256 bounced := '1';
257 bounce_speed := (not vy) + 1;
258 vy := (not vy) + 1;
259 if vy(9) = '0' and vy > 1 then
260 vy := vy - ("000" & vy(9 downto 3));
261 end if;
262 end if;
263
264 -- == LEFT WALL ==
265 -- Same underflow fix: px >= 2000 means wrapped below zero.
266 if px >= UNDERFLOW or px <= LEFT_WALL + SIZE11 then
267 px := LEFT_WALL + SIZE11;
268 bounced := '1'; bounce_wall := '1';
269 bounce_speed := (not vx) + 1;
270 vx := (not vx) + 1;
271 if vx(9) = '0' and vx > 1 then
272 vx := vx - ("000" & vx(9 downto 3));
273 end if;
274 end if;
275
276 -- == RIGHT WALL ==
277 if px >= RIGHT_WALL - SIZE11 then
278 px := RIGHT_WALL - SIZE11;
279 bounced := '1'; bounce_wall := '1';
280 bounce_speed := vx;
281 vx := (not vx) + 1;
282 if vx(9) = '1' then
283 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
284 end if;
285 if vx(9) = '1' and vx >= CONV_STD_LOGIC_VECTOR(1022, 10) then
286 vx := (others => '0');
287 end if;
288 end if;
289
290 -- == OBSTACLE COLLISIONS ==
291 -- Use previous-frame bounds to detect which face was entered.
292 -- This avoids the overlap-axis bug where a fast horizontal move into
293 -- a thin platform's side gets incorrectly resolved as a vertical hit.
294 c_prev_top := pos_y - SIZE11;
295 c_prev_bot := pos_y + SIZE11;
296 c_prev_left := pos_x - SIZE11;
297 c_prev_right := pos_x + SIZE11;
298
299 for obs_i in 0 to OBS_COUNT-1 loop
300 c_left := px - SIZE11;
301 c_right := px + SIZE11;
302 c_top := py - SIZE11;
303 c_bot := py + SIZE11;
304
305 -- 1. Swept Horizontal Overlap
306 if vx(9) = '0' then -- moving right
307 x_overlap := (c_right >= OBS_L(obs_i)) and (c_prev_left <= OBS_R(obs_i));
308 else -- moving left
309 x_overlap := (c_prev_right >= OBS_L(obs_i)) and (c_left <= OBS_R(obs_i));
310 end if;
311
312 -- 2. Swept Vertical Overlap
313 if vy(9) = '0' then -- moving down (gravity is positive)
314 y_overlap := (c_bot >= OBS_T(obs_i)) and (c_prev_top <= OBS_B(obs_i));
315 else -- moving up
316 y_overlap := (c_prev_bot >= OBS_T(obs_i)) and (c_top <= OBS_B(obs_i));
317 end if;
318
319 -- 3. Check collision using the swept bounds
320 if x_overlap and y_overlap then
321
322 if c_prev_bot <= OBS_T(obs_i) then
323 -- Entered from top → land on surface
324 py := OBS_T(obs_i) - SIZE11; grounded := '1';
325 bounced := '1';
326 vy := (not vy) + 1;
327 if vy(9) = '0' and vy > 1 then
328 vy := vy - ("000" & vy(9 downto 3));
329 elsif vy(9) = '1' and vy < CONV_STD_LOGIC_VECTOR(1022, 10) then
330 vy := vy + ("000" & ((not vy(9 downto 3)) + 1));
331 end if;
332 if vy(9) = '0' and vy < 2 then vy := (others => '0'); end if;
333 if vy(9) = '1' and vy >= CONV_STD_LOGIC_VECTOR(1022, 10) then
334 vy := (others => '0');
335 end if;
336
337 elsif c_prev_top >= OBS_B(obs_i) then
338 -- Entered from bottom → bump head on underside
339 py := OBS_B(obs_i) + SIZE11;
340 bounced := '1';
341 vy := (not vy) + 1;
342 if vy(9) = '0' and vy > 1 then
343 vy := vy - ("000" & vy(9 downto 3));
344 end if;
345
346 elsif c_prev_right <= OBS_L(obs_i) then
347 -- Entered from left → bounce off left face
348 px := OBS_L(obs_i) - SIZE11;
349 bounced := '1'; bounce_wall := '1';
350 vx := (not vx) + 1;
351 if vx(9) = '0' and vx > 1 then
352 vx := vx - ("000" & vx(9 downto 3));
353 elsif vx(9) = '1' and vx < CONV_STD_LOGIC_VECTOR(1022, 10) then
354 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
355 end if;
356
357 elsif c_prev_left >= OBS_R(obs_i) then
358 -- Entered from right → bounce off right face
359 px := OBS_R(obs_i) + SIZE11;
360 bounced := '1'; bounce_wall := '1';
361 vx := (not vx) + 1;
362 if vx(9) = '0' and vx > 1 then
363 vx := vx - ("000" & vx(9 downto 3));
364 elsif vx(9) = '1' and vx < CONV_STD_LOGIC_VECTOR(1022, 10) then
365 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
366 end if;
367
368 else
369 -- Corner / spawn-inside fallback: resolve by velocity direction
370 if vy(9) = '0' then py := OBS_T(obs_i) - SIZE11; grounded := '1';
371 else py := OBS_B(obs_i) + SIZE11; end if;
372 bounced := '1';
373 vy := (not vy) + 1;
374 if vy(9) = '0' and vy > 1 then
375 vy := vy - ("000" & vy(9 downto 3));
376 end if;
377 end if;
378 end if;
379 end loop;
380
381 -- == HOLD-SLAM LANDING BOOST ==
382 -- Bigger boost when slam started closer to the surface
383 if grounded = '1' and slam_held = '1' then
384 slam_dist := py - slam_start_y;
385 if slam_dist < SLAM_CLOSE_THR then
386 slam_boost := SLAM_BOOST_CLOSE;
387 elsif slam_dist < SLAM_MED_THR then
388 slam_boost := SLAM_BOOST_MED;
389 else
390 slam_boost := SLAM_BOOST_FAR;
391 end if;
392 vy := vy - slam_boost;
393 slam_held <= '0';
394 end if;
395
396 -- == COMMIT ==
397 vel_x <= vx;
398 vel_y <= vy;
399 pos_x <= px;
400 pos_y <= py;
401
402 -- == CAMERA: lag-follow character (1/8 of gap per frame, min 1px) ==
403 -- Compute clamped target
404 if px < CONV_STD_LOGIC_VECTOR(320, 11) then
405 tcx := (others => '0');
406 elsif px > CONV_STD_LOGIC_VECTOR(1180, 11) then
407 tcx := CONV_STD_LOGIC_VECTOR(860, 11);
408 else
409 tcx := px - CONV_STD_LOGIC_VECTOR(320, 11);
410 end if;
411
412 if py < CONV_STD_LOGIC_VECTOR(240, 11) then
413 tcy := (others => '0');
414 elsif py > CONV_STD_LOGIC_VECTOR(1260, 11) then
415 tcy := CONV_STD_LOGIC_VECTOR(1020, 11);
416 else
417 tcy := py - CONV_STD_LOGIC_VECTOR(240, 11);
418 end if;
419
420 -- Slide camera toward target
421 if cam_x_sig < tcx then
422 dcx := tcx - cam_x_sig;
423 if dcx(10 downto 3) = "00000000" then
424 cam_x_sig <= cam_x_sig + 1;
425 else
426 cam_x_sig <= cam_x_sig + ("000" & dcx(10 downto 3));
427 end if;
428 elsif cam_x_sig > tcx then
429 dcx := cam_x_sig - tcx;
430 if dcx(10 downto 3) = "00000000" then
431 cam_x_sig <= cam_x_sig - 1;
432 else
433 cam_x_sig <= cam_x_sig - ("000" & dcx(10 downto 3));
434 end if;
435 end if;
436
437 if cam_y_sig < tcy then
438 dcy := tcy - cam_y_sig;
439 if dcy(10 downto 3) = "00000000" then
440 cam_y_sig <= cam_y_sig + 1;
441 else
442 cam_y_sig <= cam_y_sig + ("000" & dcy(10 downto 3));
443 end if;
444 elsif cam_y_sig > tcy then
445 dcy := cam_y_sig - tcy;
446 if dcy(10 downto 3) = "00000000" then
447 cam_y_sig <= cam_y_sig - 1;
448 else
449 cam_y_sig <= cam_y_sig - ("000" & dcy(10 downto 3));
450 end if;
451 end if;
452
453 -- == SQUISH ==
454 if bounced = '1' and bounce_speed > 3 then
455 if bounce_speed >= 8 then squish <= "1000";
456 else squish <= bounce_speed(3 downto 0); end if;
457 squish_h <= bounce_wall;
458 elsif squish > 0 then
459 squish <= squish - 1;
460 end if;
461
462 if grounded = '1' then on_ground <= '1';
463 else on_ground <= '0'; end if;
464
465 end process physics;
466
467end behavior;