From ba0cf9e5293f258c58ced1fcc47f558e7419dae4 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Mon, 10 Mar 2025 22:55:41 +0100 Subject: Provide an example on multiscreen scrolling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- scroll/include/collision.s | 234 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 scroll/include/collision.s (limited to 'scroll/include/collision.s') diff --git a/scroll/include/collision.s b/scroll/include/collision.s new file mode 100644 index 0000000..37debc5 --- /dev/null +++ b/scroll/include/collision.s @@ -0,0 +1,234 @@ +;;; +;; Collisions for the background are handled by building up an array starting at +;; RAM address $0700. This array contains a list of values to be considered when +;; checking up background collision, and it's expected to be built when loading +;; a screen or parts of it (e.g. `load_column`). +;; +;; In summary, each row of metatiles is mapped by two bytes. There are 16 tiles +;; in a row (see `metatile.s`), so each bit from these two bytes map whether +;; that metatile has a collision block or not. More specifically, the first bit +;; from the first byte maps the first metatile on that row, the second bit from +;; the first byte the second metatile on that row, and so on. Considering that +;; there are 15 rows of tiles * 2 bytes for each row, it means that we can get a +;; whole screen mapped with 30 bytes. As explained on the `metatile.s` file and +;; taking the example of Super Mario Bros., things could have been further +;; compressed by disregarding some of the rows which are anyways unaccessible by +;; the player (e.g. by the HUD), but I have tried to keep things generic. +;; +;; Last but not least, we reserve space for two screens: one per nametable. +;; Thus, the memory usage looks like this: +;; +;; - $0700-$071D: collision map for the screen on nametable 0. +;; - $071E-$073B: collision map for the screen on nametable 1. +.scope Collision + ;; The high byte value for the collision map (which lives at $0700 in RAM). + COLLISION_MAP_HI = $07 + + ;; Starting index for the collision map of the second screen. + COLLISION_MAP_SECOND = $1E + + ;; Shadow references to Player::zp_screen_{x,y} and Background::zp_scroll. + ;; Not awesome, but cc65 doesn't allow to re-open scopes or similar + ;; code-sharing scenarios. + zp_player_screen_x = $30 + zp_player_screen_y = $31 + zp_background_scroll = $95 + + ;; Set/unset the proper bit on the collision map for the given metatile + ;; position and collision bit. + ;; + ;; The `a` register has to contain the collision information as given on a + ;; metatile reference for a screen. Hence, something like `$81` informs that + ;; metatile with id 1 is a solid block, while `$01` would mean the same but + ;; unsetting collision for that block. + ;; + ;; In memory you have to pass `Globals::zp_arg0` and `Globals::zp_arg1`, + ;; which contain the Y and X metatile coordinates as passed around in + ;; `Buffer::push_metatile`. This function won't touch other parameters, so + ;; future code can rely on `Globals::zp_arg2` and the rest to be preserved + ;; (i.e. only `Globals::zp_arg0` and `Globals::zp_arg1` will be affected). + .proc set_background_collision + ;; Preserve the collision byte for later. + sta Globals::zp_tmp0 + + ;; Select the screen to be used and save it into the `y` register, as + ;; expected by the `get_background_collision_y` function. + lda Globals::zp_flags + and #%00000100 + lsr + lsr + tay + + ;; Get the bitmap address for the Y and X coordinates. + jsr get_background_collision_y + tax + + ;; Should we set or unset the collision bit? + lda Globals::zp_tmp0 + bmi @set_collision + + ;; We have to unset it: flip the mask, unset the given bit and save the + ;; result. + txa + eor #$FF + and (Globals::zp_arg0), y + sta (Globals::zp_arg0), y + rts + + @set_collision: + ;; We have to set it: apply the given mask and save the result. + txa + ora (Globals::zp_arg0), y + sta (Globals::zp_arg0), y + rts + .endproc + + ;; Get the collision value for the Y and X positions of a metatile. These + ;; two coordinates are given in memory arguments in this exact order, while + ;; the `y` register selects which map to pick from (i.e. the screen). + ;; + ;; Returns two bytes in memory which correspond to the 16-bit pointer to the + ;; row section that contains this metatile (remember that a row of metatiles + ;; in screen is split in two). This pointer is complemented by the fact that + ;; the `y` register will be set to the proper value. Hence, after calling + ;; this function you can get the collision bitmap for the row section with: + ;; + ;; lda (Globals::zp_arg0), y + ;; + ;; The `a` register will contain the mask which pin points the exact + ;; location on the X coordinate. This is useful for collision detection. + ;; Hence, combining with the above you can do: + ;; + ;; jsr get_background_collision_y + ;; and (Globals::zp_arg0), y ; Use the mask on `a` against the bitmap. + ;; bne @collision ; Collision! + ;; + .proc get_background_collision_y + ;; Each row is made up of two bytes worth of bitmaps. Hence, the Y + ;; metatile position has to be multiplied by two to get to the proper + ;; row. + lda Globals::zp_arg0 + asl + + ;; Depending on the nametable being mapped, we have to pick one map or + ;; the other. Since they are contiguous, this is a matter of simply + ;; adding the base address for the second map if that's the case. + cpy #1 + bne @store_base_pointer + clc + adc #COLLISION_MAP_SECOND + @store_base_pointer: + sta Globals::zp_arg0 + + ;; Now it's time to figure things out given the value for the X + ;; coordinate. First of all, set on the `y` register which bitmap to + ;; pick for the row of metatiles (remember that a row is split in two + ;; bitmaps). Moreover, update the coordinate if it's on the second + ;; bitmap. + lda Globals::zp_arg1 + ldy #0 + cmp #8 + bcc @eval_x + sec + sbc #8 + iny + + ;; With that, now we have to shift as many times as the X coordinate to + ;; get the mask for the bitmap we are pointing at. + @eval_x: + tax + lda #1 + cpx #0 + beq @end + @loop: + asl + dex + bne @loop + + @end: + ;; The only thing missing is to set the high byte of the end 16-bit + ;; pointer. + ldx #COLLISION_MAP_HI + stx Globals::zp_arg1 + + ;; NOTE: Returned value is left untouched into the `a` register, + ;; computed in the previous loop. + + rts + .endproc + + ;; Translates the player's screen coordinates into metatile ones. + ;; + ;; This function takes two memory arguments, which are offsets to the Y and + ;; X screen coordinates respectively. The screen coordinates are taken + ;; directly from `Player::zp_screen_{y,x}`, and it also handles + ;; `Background::zp_scroll`. + ;; + ;; In turn this function will store in `zp_arg0` the Y metatile coordinate, + ;; and in `zp_arg1` the X one. The `y` register will also point if we are in + ;; nametable 0 or 1. + .proc screen_to_mt_coordinates + ;; Add up the player's screen coordinates with the given Y offset. + ;; Transforming that result into a metatile index is basically a matter + ;; of shifting right 4 times, since metatiles are 16x16 pixels. + lda Globals::zp_arg0 + clc + adc Collision::zp_player_screen_y + lsr + lsr + lsr + lsr + sta Globals::zp_arg0 + + ;; The `y` register is generally the nametable where the focus is on. + ;; This is handled on the PPU control register, and shadowed by a + ;; `PPU::zp_control` variable. This will have to be adjusted depending + ;; on the current scroll. See more below. + lda #%00000001 + and PPU::zp_control + tay + + ;; Before figuring out the X metatile coordinates, compute the metatile + ;; coordinates if we were only to consider the scroll. This gives us + ;; information on from which metatile column is the current nametable + ;; visible. It will be used to correct the `y` register whenever columns + ;; from the next nametable start appearing on the player's area. + lda Collision::zp_background_scroll + lsr + lsr + lsr + lsr + sta Globals::zp_tmp0 + + ;; The coordinates for the X axis is similar to the Y one but we also + ;; need to take into account scrolling. Note that overflow is permitted + ;; as this will be handled by the correction code below (i.e. if the X + ;; metatile is beyond what can be seen by the current nametable, focus + ;; on the next one). + lda Globals::zp_arg1 + clc + adc Collision::zp_player_screen_x + clc + adc Collision::zp_background_scroll + lsr + lsr + lsr + lsr + sta Globals::zp_arg1 + + ;; Is the X metatile coordinate actually smaller than the scrolling + ;; view? + cmp Globals::zp_tmp0 + bcs @end + + ;; Yes! Then we are on the next nametable. Amend the value on the `y` + ;; register and make sure it's either 1 or 0. + iny + tya + and #%00000001 + tay + + @end: + rts + .endproc +.endscope -- cgit v1.2.3