diff --git a/physics_engine.vhd b/physics_engine.vhd index be47e9c..ce906c7 100644 --- a/physics_engine.vhd +++ b/physics_engine.vhd @@ -29,7 +29,9 @@ entity physics_engine is char_height : out std_logic_vector(9 downto 0); cam_x : out std_logic_vector(10 downto 0); cam_y : out std_logic_vector(10 downto 0); - vel_out : out std_logic_vector(9 downto 0) + vel_out : out std_logic_vector(9 downto 0); + vel_x_out : out std_logic_vector(9 downto 0); + vel_y_out : out std_logic_vector(9 downto 0) ); end physics_engine; @@ -82,10 +84,12 @@ architecture behavior of physics_engine is begin - char_x <= pos_x; - char_y <= pos_y; - cam_x <= cam_x_sig; - cam_y <= cam_y_sig; + char_x <= pos_x; + char_y <= pos_y; + cam_x <= cam_x_sig; + cam_y <= cam_y_sig; + vel_x_out <= vel_x; + vel_y_out <= vel_y; -- Absolute values of both velocity axes. abs_vel_x <= (not vel_x) + 1 when vel_x(9) = '1' else vel_x; diff --git a/renderer.vhd b/renderer.vhd index f24fb95..f3cd358 100644 --- a/renderer.vhd +++ b/renderer.vhd @@ -4,6 +4,10 @@ -- world_row = pixel_row + cam_y -- All comparisons in world-space (11-bit). -- Obstacles imported from level package (use work.level.all). +-- +-- Color priority: +-- Arrow = White (111) > Char = Red (100) > Obs = Yellow (110) +-- > Ground/Ceil/Walls = Green (010) > Trail = Magenta (101) > Sky = Black (000) -- ======================================================================== library IEEE; @@ -22,6 +26,9 @@ entity renderer is char_height : in std_logic_vector(9 downto 0); cam_x : in std_logic_vector(10 downto 0); cam_y : in std_logic_vector(10 downto 0); + vel_x_in : in std_logic_vector(9 downto 0); + vel_y_in : in std_logic_vector(9 downto 0); + trail_on_in : in std_logic; red : out std_logic; green : out std_logic; blue : out std_logic @@ -36,18 +43,23 @@ architecture behavior of renderer is constant LEFT_WALL : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(8, 11); constant RIGHT_WALL : std_logic_vector(10 downto 0) := CONV_STD_LOGIC_VECTOR(1492, 11); - signal char_on, ground_on, wall_on, ceiling_on, obs_on : std_logic; + -- World-space pixel coordinates (shared between processes) + signal wc : std_logic_vector(10 downto 0); + signal wr : std_logic_vector(10 downto 0); + + signal char_on, ground_on, wall_on, ceiling_on, obs_on, arrow_on : std_logic; begin - render : process(pixel_row, pixel_column, char_x, char_y, - char_width, char_height, cam_x, cam_y) - variable wc : std_logic_vector(10 downto 0); - variable wr : std_logic_vector(10 downto 0); - begin - wc := ('0' & pixel_column) + cam_x; - wr := ('0' & pixel_row) + cam_y; + -- Convert screen coordinates to world coordinates + wc <= ('0' & pixel_column) + cam_x; + wr <= ('0' & pixel_row) + cam_y; + -- ---------------------------------------------------------------- + -- Scene geometry: character, world edges, obstacles + -- ---------------------------------------------------------------- + render : process(wc, wr, char_x, char_y, char_width, char_height) + begin char_on <= '0'; ground_on <= '0'; wall_on <= '0'; @@ -77,19 +89,77 @@ begin end process render; - -- Priority: char=Red(100), obstacles=Yellow(110), ground/ceil/walls=Green(010), sky=Black - color : process(char_on, ground_on, wall_on, ceiling_on, obs_on) + -- ---------------------------------------------------------------- + -- Velocity vector arrow + -- Line from (char_x, char_y) to (char_x + 3*vx, char_y + 3*vy). + -- Uses cross-product line-segment test: + -- pixel on segment iff |dy*(px-x0) - dx*(py-y0)| <= max(|dx|,|dy|) + -- and dx*(px-x0) + dy*(py-y0) in [0, dx^2+dy^2] + -- ---------------------------------------------------------------- + arrow_proc : process(wc, wr, char_x, char_y, vel_x_in, vel_y_in) + variable wci, wri : integer; + variable cx, cy : integer; + variable vxi, vyi : integer; + variable dx, dy : integer; + variable cross : integer; + variable dot_p : integer; + variable len_sq : integer; + variable maxd : integer; + begin + wci := CONV_INTEGER(wc); + wri := CONV_INTEGER(wr); + cx := CONV_INTEGER(char_x); + cy := CONV_INTEGER(char_y); + + -- Convert 10-bit 2's complement velocity to signed integer + if vel_x_in(9) = '1' then vxi := CONV_INTEGER(vel_x_in) - 1024; + else vxi := CONV_INTEGER(vel_x_in); end if; + if vel_y_in(9) = '1' then vyi := CONV_INTEGER(vel_y_in) - 1024; + else vyi := CONV_INTEGER(vel_y_in); end if; + + -- Scale arrow by 3 for visibility + dx := vxi * 3; + dy := vyi * 3; + + -- Precompute line metrics + cross := dy * (wci - cx) - dx * (wri - cy); + dot_p := dx * (wci - cx) + dy * (wri - cy); + len_sq := dx * dx + dy * dy; + + -- Chebyshev thickness (~1-2 world-pixels wide) + maxd := dx; if maxd < 0 then maxd := -maxd; end if; + if dy > maxd then maxd := dy; + elsif -dy > maxd then maxd := -dy; end if; + + arrow_on <= '0'; + if maxd > 4 then -- skip arrow when velocity is negligible + if cross >= -maxd and cross <= maxd and + dot_p >= 0 and dot_p <= len_sq then + arrow_on <= '1'; + end if; + end if; + end process arrow_proc; + + -- ---------------------------------------------------------------- + -- Color output with priority + -- ---------------------------------------------------------------- + color : process(char_on, ground_on, wall_on, ceiling_on, + obs_on, arrow_on, trail_on_in) begin - if char_on = '1' then - red <= '1'; green <= '0'; blue <= '0'; + if arrow_on = '1' then + red <= '1'; green <= '1'; blue <= '1'; -- White + elsif char_on = '1' then + red <= '1'; green <= '0'; blue <= '0'; -- Red elsif obs_on = '1' then - red <= '1'; green <= '1'; blue <= '0'; + red <= '1'; green <= '1'; blue <= '0'; -- Yellow elsif ground_on = '1' or ceiling_on = '1' then - red <= '0'; green <= '1'; blue <= '0'; + red <= '0'; green <= '1'; blue <= '0'; -- Green elsif wall_on = '1' then - red <= '0'; green <= '1'; blue <= '0'; + red <= '0'; green <= '1'; blue <= '0'; -- Green + elsif trail_on_in = '1' then + red <= '1'; green <= '0'; blue <= '1'; -- Magenta else - red <= '0'; green <= '0'; blue <= '0'; + red <= '0'; green <= '0'; blue <= '0'; -- Black (sky) end if; end process color; diff --git a/trail.vhd b/trail.vhd new file mode 100644 index 0000000..7c66146 --- /dev/null +++ b/trail.vhd @@ -0,0 +1,89 @@ +-- ======================================================================== +-- Trail +-- Stores 16 world-space positions (updated every 3 frames = ~0.8 s at 60 Hz). +-- Combinationally checks each pixel against stored positions (radius 4 box). +-- trail_on fires for any pixel within 4 world-pixels of any stored dot. +-- ======================================================================== + +library IEEE; +use IEEE.STD_LOGIC_1164.all; +use IEEE.STD_LOGIC_ARITH.all; +use IEEE.STD_LOGIC_UNSIGNED.all; + +entity trail is + port( + vert_sync : in std_logic; + char_x : in std_logic_vector(10 downto 0); + char_y : in std_logic_vector(10 downto 0); + pixel_column : in std_logic_vector(9 downto 0); + pixel_row : in std_logic_vector(9 downto 0); + cam_x : in std_logic_vector(10 downto 0); + cam_y : in std_logic_vector(10 downto 0); + trail_on : out std_logic + ); +end trail; + +architecture behavior of trail is + + constant TRAIL_LEN : integer := 16; + constant TRAIL_DIV : integer := 3; -- update once every 3 frames + constant TRAIL_RADIUS : integer := 4; -- Chebyshev radius in world-pixels + + type pos_arr_t is array(0 to TRAIL_LEN-1) of std_logic_vector(10 downto 0); + + signal tx : pos_arr_t := (others => (others => '0')); + signal ty : pos_arr_t := (others => (others => '0')); + signal trail_valid : std_logic_vector(TRAIL_LEN-1 downto 0) := (others => '0'); + signal frame_ctr : integer range 0 to TRAIL_DIV-1 := 0; + +begin + + -- Shift register: push new position every TRAIL_DIV frames + shift : process + begin + wait until vert_sync'event and vert_sync = '1'; + + if frame_ctr = TRAIL_DIV - 1 then + for i in TRAIL_LEN-1 downto 1 loop + tx(i) <= tx(i-1); + ty(i) <= ty(i-1); + trail_valid(i) <= trail_valid(i-1); + end loop; + tx(0) <= char_x; + ty(0) <= char_y; + trail_valid(0) <= '1'; + frame_ctr <= 0; + else + frame_ctr <= frame_ctr + 1; + end if; + end process shift; + + -- Pixel check: is current pixel within TRAIL_RADIUS of any valid trail dot? + check : process(pixel_column, pixel_row, cam_x, cam_y, tx, ty, trail_valid) + variable wci, wri : integer; + variable xi, yi : integer; + variable dx, dy : integer; + variable hit : std_logic; + begin + wci := CONV_INTEGER('0' & pixel_column) + CONV_INTEGER(cam_x); + wri := CONV_INTEGER('0' & pixel_row) + CONV_INTEGER(cam_y); + hit := '0'; + + for i in 0 to TRAIL_LEN-1 loop + if trail_valid(i) = '1' then + xi := CONV_INTEGER(tx(i)); + yi := CONV_INTEGER(ty(i)); + dx := wci - xi; + dy := wri - yi; + if dx < 0 then dx := -dx; end if; + if dy < 0 then dy := -dy; end if; + if dx <= TRAIL_RADIUS and dy <= TRAIL_RADIUS then + hit := '1'; + end if; + end if; + end loop; + + trail_on <= hit; + end process check; + +end behavior; diff --git a/visualizer.py b/visualizer.py index a83fdf3..bede392 100644 --- a/visualizer.py +++ b/visualizer.py @@ -11,6 +11,7 @@ Usage: python3 visualizer.py """ +import math import pygame import sys @@ -125,14 +126,13 @@ def main(): if not keys_held['w']: jump_pressed = False - # First press on ground: full jump + # First press on ground: full jump (elif prevents double-apply on same frame) if keys_held['w'] and not jump_pressed and on_ground: vy = to_unsigned(0) vy = (vy - JUMP_FORCE) & MASK jump_pressed = True - - # Holding W while bouncing: boost each ground contact - if keys_held['w'] and jump_pressed and on_ground: + elif keys_held['w'] and jump_pressed and on_ground: + # Holding W while bouncing: boost each ground contact vy = (vy - JUMP_FORCE) & MASK # S: slam (air only) @@ -369,9 +369,23 @@ def main(): pygame.draw.rect(screen, COLOR_CHAR, (char_x - cw, char_y - ch, cw * 2, ch * 2)) - # HUD + # Velocity vector arrow (scale: 2px per unit of velocity) svx = to_signed(vel_x) svy = to_signed(vel_y) + ARROW_SCALE = 2 + ax = char_x + svx * ARROW_SCALE + ay = char_y + svy * ARROW_SCALE + if svx != 0 or svy != 0: + pygame.draw.line(screen, (255, 255, 255), (char_x, char_y), (ax, ay), 2) + # Arrowhead + angle = math.atan2(svy, svx) + head = 6 + for side in (+0.5, -0.5): + hx = ax - head * math.cos(angle + side) + hy = ay - head * math.sin(angle + side) + pygame.draw.line(screen, (255, 255, 255), (ax, ay), (int(hx), int(hy)), 2) + + # HUD info = [ f"pos: ({char_x}, {char_y}) vel: ({svx}, {svy})", f"squish: {squish} ground: {'yes' if on_ground else 'no'}",