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_FORCE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(8, 10);
56 constant JUMP_FORCE : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(13, 10);
57 constant MAX_VEL_X : std_logic_vector(9 downto 0) := CONV_STD_LOGIC_VECTOR(32, 10);
58
59 -- World bounds (11-bit)
60 constant GROUND : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1480, 11);
61 constant CEILING : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(16, 11);
62 constant LEFT_WALL : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(8, 11);
63 constant RIGHT_WALL : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1492, 11);
64
65 -- Underflow sentinel: valid positions top out at 1480 (GROUND).
66 -- CEILING(16) - max_upward(128) = -112 -> 11-bit unsigned = 1936.
67 -- So UNDERFLOW must be <= 1936 and > 1480. 1500 gives safe margin.
68 constant UNDERFLOW : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1500, 11);
69
70 -- Tune: vertical speed (0-63) at which all 10 LEDs are fully lit.
71 -- Lower = more sensitive (fewer LEDs at low speed fill up faster).
72 -- Higher = better dynamic range across the full velocity range.
73 -- Higher = less sensitive (need a harder bounce to light all LEDs).
74 constant LED_FULL_VEL : integer := 48;
75
76 -- Animation
77 signal squish : std_logic_vector(3 downto 0) := (others => '0');
78 signal squish_h : std_logic := '0';
79 signal on_ground : std_logic := '0';
80 signal jump_pressed : std_logic := '0';
81
82 signal abs_vel_x : std_logic_vector(9 downto 0);
83 signal abs_vel_y : std_logic_vector(9 downto 0);
84
85begin
86
87 char_x <= pos_x;
88 char_y <= pos_y;
89 cam_x <= cam_x_sig;
90 cam_y <= cam_y_sig;
91 vel_x_out <= vel_x;
92 vel_y_out <= vel_y;
93
94 -- Absolute values of both velocity axes.
95 abs_vel_x <= (not vel_x) + 1 when vel_x(9) = '1' else vel_x;
96 abs_vel_y <= (not vel_y) + 1 when vel_y(9) = '1' else vel_y;
97
98 -- Thermometer-encode peak speed (max of |vx|,|vy|) → horizontal LED bar.
99 -- n LEDs lit from LSB up → 0000011111 for n=5, 1111111111 for n=10.
100 vel_bar : process(abs_vel_x, abs_vel_y)
101 variable vx, vy, vmax : integer;
102 variable n : integer range 0 to 10;
103 begin
104 vx := CONV_INTEGER(abs_vel_x);
105 vy := CONV_INTEGER(abs_vel_y);
106 if vx > vy then vmax := vx; else vmax := vy; end if;
107 if vmax >= LED_FULL_VEL then n := 10;
108 else n := vmax * 10 / LED_FULL_VEL;
109 end if;
110 case n is
111 when 0 => vel_out <= "0000000000";
112 when 1 => vel_out <= "0000000001";
113 when 2 => vel_out <= "0000000011";
114 when 3 => vel_out <= "0000000111";
115 when 4 => vel_out <= "0000001111";
116 when 5 => vel_out <= "0000011111";
117 when 6 => vel_out <= "0000111111";
118 when 7 => vel_out <= "0001111111";
119 when 8 => vel_out <= "0011111111";
120 when 9 => vel_out <= "0111111111";
121 when others => vel_out <= "1111111111";
122 end case;
123 end process vel_bar;
124
125 char_width <= SIZE + ("000000" & squish) when squish_h = '0'
126 else SIZE - ("000000" & squish(3 downto 1));
127 char_height <= SIZE - ("000000" & squish(3 downto 1)) when squish_h = '0'
128 else SIZE + ("000000" & squish);
129
130 physics : process
131 variable vx, vy : std_logic_vector(9 downto 0);
132 variable px, py : std_logic_vector(10 downto 0);
133 variable bounced : std_logic;
134 variable bounce_wall : std_logic;
135 variable bounce_speed : std_logic_vector(9 downto 0);
136 variable grounded : std_logic;
137 variable c_left, c_right, c_top, c_bot : std_logic_vector(10 downto 0);
138 variable c_prev_top, c_prev_bot, c_prev_left, c_prev_right : std_logic_vector(10 downto 0);
139 variable tcx, tcy, dcx, dcy : std_logic_vector(10 downto 0);
140 begin
141 wait until vert_sync'event and vert_sync = '1';
142
143 vx := vel_x; vy := vel_y;
144 bounced := '0'; bounce_wall := '0';
145 bounce_speed := (others => '0');
146 grounded := '0';
147
148 -- == INPUT ==
149 if key_w = '0' then jump_pressed <= '0'; end if;
150
151 if key_w = '1' and jump_pressed = '0' and on_ground = '1' then
152 vy := (others => '0');
153 vy := vy - JUMP_FORCE;
154 jump_pressed <= '1';
155 end if;
156
157 if key_w = '1' and jump_pressed = '1' and on_ground = '1' then
158 vy := vy - JUMP_FORCE;
159 end if;
160
161 if key_s = '1' and on_ground = '0' then vy := vy + SLAM_FORCE; end if;
162
163 if key_a = '1' then
164 if on_ground = '1' then vx := vx - IMPULSE; else vx := vx - 2; end if;
165 end if;
166 if key_d = '1' then
167 if on_ground = '1' then vx := vx + IMPULSE; else vx := vx + 2; end if;
168 end if;
169
170 -- == GRAVITY ==
171 vy := vy + GRAVITY;
172
173 -- == FRICTION: vel -= vel/4, min 1 ==
174 if vx(9) = '0' then
175 if vx > 0 then
176 if vx(9 downto 2) = "00000000" then vx := vx - 1;
177 else vx := vx - ("00" & vx(9 downto 2)); end if;
178 end if;
179 else
180 if vx /= "0000000000" then
181 if vx(9 downto 2) = "11111111" then vx := vx + 1;
182 else vx := vx - ("11" & vx(9 downto 2)); end if;
183 end if;
184 end if;
185
186 -- == CLAMP velocities ==
187 if vx(9) = '0' and vx > MAX_VEL_X then vx := MAX_VEL_X; end if;
188 if vx(9) = '1' and vx < (not MAX_VEL_X) + 1 then vx := (not MAX_VEL_X) + 1; end if;
189 if vy(9) = '0' and vy > 80 then vy := CONV_STD_LOGIC_VECTOR(80, 10); end if;
190 if vy(9) = '1' and vy < CONV_STD_LOGIC_VECTOR(924, 10) then -- 924 = -100
191 vy := CONV_STD_LOGIC_VECTOR(924, 10);
192 end if;
193
194 -- == MOVE: sign-extend 10-bit velocity to 11-bit before adding ==
195 px := pos_x + (vx(9) & vx);
196 py := pos_y + (vy(9) & vy);
197
198 -- == GROUND ==
199 if py >= GROUND then
200 py := GROUND;
201 bounced := '1'; grounded := '1';
202 bounce_speed := vy;
203 vy := (not vy) + 1;
204 if vy(9) = '1' then
205 vy := vy + ("000" & ((not vy(9 downto 3)) + 1));
206 end if;
207 if vy(9) = '1' and vy >= CONV_STD_LOGIC_VECTOR(1022, 10) then
208 vy := (others => '0');
209 elsif vy(9) = '0' then vy := (others => '0'); end if;
210 end if;
211
212 -- == CEILING ==
213 -- Underflow detection: py >= 2000 means it wrapped below zero.
214 -- (GROUND=1480, RIGHT_WALL=1492 are both < 2000, so no false triggers.)
215 if py >= UNDERFLOW or py <= CEILING then
216 py := CEILING;
217 bounced := '1';
218 bounce_speed := (not vy) + 1;
219 vy := (not vy) + 1;
220 if vy(9) = '0' and vy > 1 then
221 vy := vy - ("000" & vy(9 downto 3));
222 end if;
223 end if;
224
225 -- == LEFT WALL ==
226 -- Same underflow fix: px >= 2000 means wrapped below zero.
227 if px >= UNDERFLOW or px <= LEFT_WALL + SIZE11 then
228 px := LEFT_WALL + SIZE11;
229 bounced := '1'; bounce_wall := '1';
230 bounce_speed := (not vx) + 1;
231 vx := (not vx) + 1;
232 if vx(9) = '0' and vx > 1 then
233 vx := vx - ("000" & vx(9 downto 3));
234 end if;
235 end if;
236
237 -- == RIGHT WALL ==
238 if px >= RIGHT_WALL - SIZE11 then
239 px := RIGHT_WALL - SIZE11;
240 bounced := '1'; bounce_wall := '1';
241 bounce_speed := vx;
242 vx := (not vx) + 1;
243 if vx(9) = '1' then
244 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
245 end if;
246 if vx(9) = '1' and vx >= CONV_STD_LOGIC_VECTOR(1022, 10) then
247 vx := (others => '0');
248 end if;
249 end if;
250
251 -- == OBSTACLE COLLISIONS ==
252 -- Use previous-frame bounds to detect which face was entered.
253 -- This avoids the overlap-axis bug where a fast horizontal move into
254 -- a thin platform's side gets incorrectly resolved as a vertical hit.
255 c_prev_top := pos_y - SIZE11;
256 c_prev_bot := pos_y + SIZE11;
257 c_prev_left := pos_x - SIZE11;
258 c_prev_right := pos_x + SIZE11;
259
260 for obs_i in 0 to OBS_COUNT-1 loop
261 c_left := px - SIZE11;
262 c_right := px + SIZE11;
263 c_top := py - SIZE11;
264 c_bot := py + SIZE11;
265
266 if c_right >= OBS_L(obs_i) and c_left <= OBS_R(obs_i) and
267 c_bot >= OBS_T(obs_i) and c_top <= OBS_B(obs_i) then
268
269 if c_prev_bot <= OBS_T(obs_i) then
270 -- Entered from top → land on surface
271 py := OBS_T(obs_i) - SIZE11; grounded := '1';
272 bounced := '1';
273 vy := (not vy) + 1;
274 if vy(9) = '0' and vy > 1 then
275 vy := vy - ("000" & vy(9 downto 3));
276 elsif vy(9) = '1' and vy < CONV_STD_LOGIC_VECTOR(1022, 10) then
277 vy := vy + ("000" & ((not vy(9 downto 3)) + 1));
278 end if;
279 if vy(9) = '0' and vy < 2 then vy := (others => '0'); end if;
280 if vy(9) = '1' and vy >= CONV_STD_LOGIC_VECTOR(1022, 10) then
281 vy := (others => '0');
282 end if;
283
284 elsif c_prev_top >= OBS_B(obs_i) then
285 -- Entered from bottom → bump head on underside
286 py := OBS_B(obs_i) + SIZE11;
287 bounced := '1';
288 vy := (not vy) + 1;
289 if vy(9) = '0' and vy > 1 then
290 vy := vy - ("000" & vy(9 downto 3));
291 end if;
292
293 elsif c_prev_right <= OBS_L(obs_i) then
294 -- Entered from left → bounce off left face
295 px := OBS_L(obs_i) - SIZE11;
296 bounced := '1'; bounce_wall := '1';
297 vx := (not vx) + 1;
298 if vx(9) = '0' and vx > 1 then
299 vx := vx - ("000" & vx(9 downto 3));
300 elsif vx(9) = '1' and vx < CONV_STD_LOGIC_VECTOR(1022, 10) then
301 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
302 end if;
303
304 elsif c_prev_left >= OBS_R(obs_i) then
305 -- Entered from right → bounce off right face
306 px := OBS_R(obs_i) + SIZE11;
307 bounced := '1'; bounce_wall := '1';
308 vx := (not vx) + 1;
309 if vx(9) = '0' and vx > 1 then
310 vx := vx - ("000" & vx(9 downto 3));
311 elsif vx(9) = '1' and vx < CONV_STD_LOGIC_VECTOR(1022, 10) then
312 vx := vx + ("000" & ((not vx(9 downto 3)) + 1));
313 end if;
314
315 else
316 -- Corner / spawn-inside fallback: resolve by velocity direction
317 if vy(9) = '0' then py := OBS_T(obs_i) - SIZE11; grounded := '1';
318 else py := OBS_B(obs_i) + SIZE11; end if;
319 bounced := '1';
320 vy := (not vy) + 1;
321 if vy(9) = '0' and vy > 1 then
322 vy := vy - ("000" & vy(9 downto 3));
323 end if;
324 end if;
325 end if;
326 end loop;
327
328 -- == COMMIT ==
329 vel_x <= vx;
330 vel_y <= vy;
331 pos_x <= px;
332 pos_y <= py;
333
334 -- == CAMERA: lag-follow character (1/8 of gap per frame, min 1px) ==
335 -- Compute clamped target
336 if px < CONV_STD_LOGIC_VECTOR(320, 11) then
337 tcx := (others => '0');
338 elsif px > CONV_STD_LOGIC_VECTOR(1180, 11) then
339 tcx := CONV_STD_LOGIC_VECTOR(860, 11);
340 else
341 tcx := px - CONV_STD_LOGIC_VECTOR(320, 11);
342 end if;
343
344 if py < CONV_STD_LOGIC_VECTOR(240, 11) then
345 tcy := (others => '0');
346 elsif py > CONV_STD_LOGIC_VECTOR(1260, 11) then
347 tcy := CONV_STD_LOGIC_VECTOR(1020, 11);
348 else
349 tcy := py - CONV_STD_LOGIC_VECTOR(240, 11);
350 end if;
351
352 -- Slide camera toward target
353 if cam_x_sig < tcx then
354 dcx := tcx - cam_x_sig;
355 if dcx(10 downto 3) = "00000000" then
356 cam_x_sig <= cam_x_sig + 1;
357 else
358 cam_x_sig <= cam_x_sig + ("000" & dcx(10 downto 3));
359 end if;
360 elsif cam_x_sig > tcx then
361 dcx := cam_x_sig - tcx;
362 if dcx(10 downto 3) = "00000000" then
363 cam_x_sig <= cam_x_sig - 1;
364 else
365 cam_x_sig <= cam_x_sig - ("000" & dcx(10 downto 3));
366 end if;
367 end if;
368
369 if cam_y_sig < tcy then
370 dcy := tcy - cam_y_sig;
371 if dcy(10 downto 3) = "00000000" then
372 cam_y_sig <= cam_y_sig + 1;
373 else
374 cam_y_sig <= cam_y_sig + ("000" & dcy(10 downto 3));
375 end if;
376 elsif cam_y_sig > tcy then
377 dcy := cam_y_sig - tcy;
378 if dcy(10 downto 3) = "00000000" then
379 cam_y_sig <= cam_y_sig - 1;
380 else
381 cam_y_sig <= cam_y_sig - ("000" & dcy(10 downto 3));
382 end if;
383 end if;
384
385 -- == SQUISH ==
386 if bounced = '1' and bounce_speed > 3 then
387 if bounce_speed >= 8 then squish <= "1000";
388 else squish <= bounce_speed(3 downto 0); end if;
389 squish_h <= bounce_wall;
390 elsif squish > 0 then
391 squish <= squish - 1;
392 end if;
393
394 if grounded = '1' then on_ground <= '1';
395 else on_ground <= '0'; end if;
396
397 end process physics;
398
399end behavior;