Forums

Sega Master System / Mark III / Game Gear
SG-1000 / SC-3000 / SF-7000 / OMV
Home - Forums - Games - Scans - Maps - Cheats - Credits
Music - Videos - Development - Hacks - Translations - Homebrew

View topic - Implement horizontal scrolling to a sonic-like platform

Reply to topic
Author Message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Implement horizontal scrolling to a sonic-like platform
Post Posted: Wed Sep 28, 2022 9:45 pm
Hello everyone!

Sorry for the wordy post, here is the tl;dr version: can anyone recommend a good resource for implementing horizontal scrolling to a sonic-like platform game made in assembly?

Here the lengthy version...

Building on top of the tutorials found in this website and the help of its community, I currently have a code that displays a static background, draws a (sonic the hedgehog) sprite at the centre of the screen, and let the player moves left or right by checking the controller input.

I am updating the Sprite Attribute Table using a buffer during VBlank as suggested by some of the experts of this forum and as described in hang-om's tutorial.

Now, I thought the next step would be to make the scene scrolling as the player moves. I would like to achieve an effect similar to (guess what?) Sonic the hedgehog, where the player is kind of fixed at the centre of the scene while the background scrolls left or right depending on its movement. The nice thing about Sonic the hedgehog scrolling is that it seems the character is not entirely fixed at the centre, but could move past it just a bit to give a nice movement effect (I hope you get what I mean).

Let's say I want to keep things simple and start with an entirely centered character. I think what I need to do is, instead of updating the character's X and Y position at each frame, I should move my scene instead, in a direction that is opposite to the player's one.

I am kind of stuck as to how to move on. I was reading Charles MacDonald's VDP docs (horizontal scrolling section), but it is still a bit too hard for me to understand. At least I know I am supposed to use Register $08 for this, although I am not quite sure. I also tried to go through the various posts searching by keywords but I could not find what I needed so far.

Any help (hint, suggestion, resources) will be very much appreciated! And if you arrived here... Thanks for taking the time to read through the whole post!
  View user's profile Send private message
  • Joined: 05 Sep 2013
  • Posts: 3259
  • Location: Torino, then London, now Stockholm
Reply with quote
Post Posted: Thu Sep 29, 2022 8:16 am
umbe1987 wrote
Let's say I want to keep things simple and start with an entirely centered character. I think what I need to do is, instead of updating the character's X and Y position at each frame, I should move my scene instead, in a direction that is opposite to the player's one.


Correct, and this is likely the best way to approach the whole 'I want the background to scroll' process.

So, the hardware provides you a way to scroll the background by using the horizontal scroll register. What you would get by using that is simply have your background image wrap around the screen edges, that is - what goes out from the left comes in from the right and vice-versa.

I would try to implement that first. Have the background go the 'opposite' direction, grasp this basic concept and we'll discuss further.
  View user's profile Send private message Visit poster's website
  • Joined: 29 Mar 2012
  • Posts: 774
  • Location: Spain
Reply with quote
Post Posted: Thu Sep 29, 2022 8:23 am
I didn't get exactly what's the problem you're having, but I'll try to guess.

I think you don't understand how scroll works in SMS. You can update the scroll x value in the register $08 to put a 0-255 value for the scroll x. This is not something like the scroll x value of a level, it only sets which pixel column will be printed at the left border of the screen. You should then take care of updating the column of tiles just starting to appear on the right side of the screen. There's a really useful register to hide the left-most column in the screen, so you can update it without being apparent.

The easiest way to understand what I said in the last paragraph is to open Sonic in Emulicious or Meka, open the tilemap viewer, and si how the viewing window moves around.

That being said, if your doubt is about how to implement level scrolling in an efficient way, you can take a look at GSlib https://bitbucket.org/Psidum/gslib/src/master/
  View user's profile Send private message
  • Joined: 23 Mar 2013
  • Posts: 609
  • Location: Copenhagen, Denmark
Reply with quote
Post Posted: Thu Sep 29, 2022 5:36 pm
sverx wrote
So, the hardware provides you a way to scroll the background by using the horizontal scroll register. What you would get by using that is simply have your background image wrap around the screen edges, that is - what goes out from the left comes in from the right and vice-versa.

I would try to implement that first. Have the background go the 'opposite' direction, grasp this basic concept and we'll discuss further.


If you want to experiment the way sverx suggests, you can start out by tweaking the Racer code you already know:

       ld a,(scroll)       ; 1-byte scroll reg. buffer in ram.
       ld b,9              ; target VDP register 9 (v-scroll).
       call setreg         ; now vdp register = buffer, and the
                           ; screen scrolls accordingly.


Racer scrolls vertically. If you have implemented the setreg function from the tutorial, you can try incrementing or decrementing the value at [scroll], and set register 8 instead of 9:

       ld a,(scroll)       ; 1-byte scroll reg. buffer in ram.
       ld b,8              ; target VDP register 8 (h-scroll).
       call setreg         ; now vdp register = buffer, and the
                           ; screen scrolls accordingly.

As sverx points out, this will of course not scroll new stuff into the screen, but it might be a good place to start the quest for Sonic-like scrolling.
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Wed Oct 26, 2022 8:54 pm
Thank you for the many replies, I finally had the time to go through them and try to implement some new code (I know I am super late, but although I would love to code every night, unfortunately I don't have the time... but I am coming back to it when I can and keep trying ;).

First of all @hang-on, I did use your tutorial (a lot) after reading @maxim's one. Therefore, some of the Helper functions I have come [email protected]'s tutorial, and I try to integrate new ones when it's needed.

For example, instead of using your "setreg", I am (trying) to use @maxim's "SetVDPAddress", although I am not totally sure they do the same.

So, instead of:


setreg out ($bf),a         ; output command word 1/2.
ld a,$80
or b
out ($bf),a         ; output command word 2/2.
ret


I am doing:


SetVDPAddress:
; Sets the VDP address
; Parameters: hl = address
    push af
        ld a,l
        out (VDPControl),a
        ld a,h
        out (VDPControl),a
    pop af
    ret


I am banging my head to make it work, since (I admit) after some tries I switched blindly to your "setreg" and the scroll worked (and this is the great news!).

But I'd like to know why it is not working with the "SetVDPAddress"
way.

The way I am calling it is like:


HScrollReg equ 8
ld a,(scroll)      ; Scroll background - update the horizontal scroll buffer.
add 1               ; add constant to scroll
ld (scroll),a      ; update scroll buffer.
ld hl,HScrollReg
call SetVDPAddress


I guess the problems might be: 1) the two helper functions are NOT the same; or 2) I am calling SetVDPAddress the wrong way.

Also, in "setreg", what is the "ld a,$80" supposed to do?

Thanks to anyone who might help.
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Wed Oct 26, 2022 9:18 pm
Ok, I just realized that probably, @maxim's "SetVDPAddress" is NOT = to @hang-on's "setreg", BUT more to @hang-on's "vrampr", although "SetVDPAddress" seems more "general" in the sense that it can also set a CRAM write address rather than only a VRAM one (MAYBE?).

vrampr push af
       ld a,l
       out ($bf),a
       ld a,h
       or $40
       out ($bf),a
       pop af
       ret


As far as I can tell, @maxim's tutorial does not have something like @hang-on's "setreg" so it makes sense to introduce this new helper function into my code.

I am still a bit curious on what that "ld a,$80" is doing, I can only see from one comment in @hang-on's tutorial that it MIGHT be the "VDP register command byte".

EDIT

BTW, just wanted to say a big thanks to all of you, I am so excited I am progressing on this, and it's all thanks to you!
  View user's profile Send private message
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Thu Oct 27, 2022 1:06 am
The $80 is being bitwise or-ed with the value in CPU register b.

The VDP looks at the top two bits in the command byte in order to figure out what to do with it. If the top two bits are 10 it takes the remaining 6 bits and sets that VDP register value to the last byte you sent it.

The $80 comes from the top two bits being 10.

Similarly when you or with $40 that corresponds to the top two bits being 01 which the VDP understands to mean "write to this VRAM address". Again when you or with $c0 that corresponds to the top two bits being 11 which the VDP understands to mean "write to this CRAM" address. Finally if the top two bits are 00 that means read from VRAM.
  View user's profile Send private message Visit poster's website
  • Joined: 23 Mar 2013
  • Posts: 609
  • Location: Copenhagen, Denmark
Reply with quote
Post Posted: Thu Oct 27, 2022 6:33 am
More on the topic of working with VDP registers, including the command bytes and bits: I have used the original Sega documentation countless times: [url] https://www.smspower.org/Development/SMSOfficialDocs#VDPREGISTERS [/url]. I’m on my second print copy, due to wear :)
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Fri Nov 04, 2022 8:39 pm
sverx wrote
umbe1987 wrote
Let's say I want to keep things simple and start with an entirely centered character. I think what I need to do is, instead of updating the character's X and Y position at each frame, I should move my scene instead, in a direction that is opposite to the player's one.


Correct, and this is likely the best way to approach the whole 'I want the background to scroll' process.

So, the hardware provides you a way to scroll the background by using the horizontal scroll register. What you would get by using that is simply have your background image wrap around the screen edges, that is - what goes out from the left comes in from the right and vice-versa.

I would try to implement that first. Have the background go the 'opposite' direction, grasp this basic concept and we'll discuss further.


Ok, so I understood now how to make a 32x28 tiles background scroll horizontally.

In a previous failed attempt, I also learned that I cannot simply load a huge tilemap and scroll it, because (quoting maxim)
Quote
the image is much bigger than the video RAM can hold.


So, my idea for the horizontal scrolling using a bigger image now would be (please correct me if I'm wrong):
    include a big background asset (I prepared on whose size is 96x28 tiles)

    load only the first 32x28 tiles from it

    if the user wants to move right, I'd show (load) the 33th column of tilemap


If the user wants to move left and I am at the beginning of the scene, I'd be happy for now if the image wraps around the final column.

The problem is, I don't really know how the data is read. In other words, my code is loading the data like this:

ld hl,$3800 | VRAMWrite
call SetVDPAddress
; 2. Output tilemap data
ld hl,TileMapData
ld bc,TileMapDataEnd-TileMapData  ; Counter for number of bytes to write
call CopyToVDP


What I would change is instead of reaching "TileMapDataEnd", I'd stop before (at 32th column) and keep track on the last loaded column, and loading the one after as the game needs to scroll, but I am unsure as to how to limit the load of the tilemap data.

I tried with this probably stupid attempt and obviously it did not work (the output is below the code):

ld hl,TileMapData
ld bc,(TileMapData+1792)-TileMapData  ; Counter for number of bytes to write
call CopyToVDP


output


Why 1792? Because from tutorial lesson 1 of maxim I know
Quote
The tilemap is usually stored in VRAM at location $3800, and takes up 1792 bytes (32×28×2 bytes).


Finally, I am attaching the compete background I am including as my tilemap data (converted with BMP2TILE v.0.61).
failed_bg_attempt.png (86.33 KB)
failed_bg_attempt.png

  View user's profile Send private message
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Fri Nov 04, 2022 9:14 pm
umbe1987 wrote

ld hl,TileMapData
ld bc,(TileMapData+1792)-TileMapData  ; Counter for number of bytes to write
call CopyToVDP


Depends on what assembler you're using, but I'd be wary of writing an expression like the one you have used on the second line. Parentheses in assembly (at least z80 assembly here) usually mean indirect addressing and some assemblers will complain or possibly do something unexpected if you use them as you have. Additionally you want to be mindful of the word size as if the parentheses did act in the way you intended there could be situations where the addition caused the result to truncate unexpectedly. I think you're probably fine here, but in any case the (TileMapData+1792)-TileMapData is needlessly complex - it simplifies to just 1792 so I would highly recommend you just write ld bc, 1792 to eliminate that as a source of problems.

Otherwise, how are you getting on learning to use the debugger in Emulicious? I'll be honest, I end up in situations like this in assembler ALL the time and sometimes a few steps with the debugger is all it takes to figure it out, compared with potentially hours of code inspection...
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14225
  • Location: London
Reply with quote
Post Posted: Fri Nov 04, 2022 10:22 pm
If you have the raw tilemap data in ROM, it’s an array of words - two bytes for each 8x8 tile on the screen. If the array is (for example) 100x100 words, and you want to draw the top left area, then you want to load the first 32 words (64 bytes); then index 100..131 (byte offset 200 to 263); then 200..231, and so on. To scroll right you need to pluck specific words out of the array to match the location of the tiles you want to show. I suggest you draw it out on paper and figure out the maths to turn a scroll amount into the indices of the tiles you want to load for both horizontal and vertical movements in either direction.
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Fri Nov 04, 2022 10:58 pm
willbritton wrote
umbe1987 wrote

ld hl,TileMapData
ld bc,(TileMapData+1792)-TileMapData  ; Counter for number of bytes to write
call CopyToVDP


Depends on what assembler you're using, but I'd be wary of writing an expression like the one you have used on the second line. Parentheses in assembly (at least z80 assembly here) usually mean indirect addressing and some assemblers will complain or possibly do something unexpected if you use them as you have. Additionally you want to be mindful of the word size as if the parentheses did act in the way you intended there could be situations where the addition caused the result to truncate unexpectedly. I think you're probably fine here, but in any case the (TileMapData+1792)-TileMapData is needlessly complex - it simplifies to just 1792 so I would highly recommend you just write ld bc, 1792 to eliminate that as a source of problems.

Otherwise, how are you getting on learning to use the debugger in Emulicious? I'll be honest, I end up in situations like this in assembler ALL the time and sometimes a few steps with the debugger is all it takes to figure it out, compared with potentially hours of code inspection...


Hi @willbritton, and thanks for showing up for helping me as always!

First of all, you're totally right, I could have written "ld bc, 1792" instead (although in my case the compiler vasm did not seem to output a different image, so I guess in my case the parentheses were fine, but I agree i should avoid them).

I have to admit I found it hard to understand the way the debugger works, but I gave it another try now and I think I am starting to understand how I am supposed to use it.

I kept the background I was using in my previous successful attempt and had a look at the values of HL and BC from the debugger: BC (my counter) was actually $0700 (decimal 1792), so I think I was thinking fine there, while HL was $01ED (decimal 493).

Now, when I use the new image, the one I attached in my previous post, the values of BC and HL are actually the same.



Anyway, I think I need to investigate more and hopefully understand better how the debugger works. Thanks for the suggestion (and if you or anyone else have another one I'd be happy to hear;) )!
debugger.png (186.85 KB)
debugger.png

  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Fri Nov 04, 2022 11:04 pm
Maxim wrote
If you have the raw tilemap data in ROM, it’s an array of words - two bytes for each 8x8 tile on the screen. If the array is (for example) 100x100 words, and you want to draw the top left area, then you want to load the first 32 words (64 bytes); then index 100..131 (byte offset 200 to 263); then 200..231, and so on. To scroll right you need to pluck specific words out of the array to match the location of the tiles you want to show. I suggest you draw it out on paper and figure out the maths to turn a scroll amount into the indices of the tiles you want to load for both horizontal and vertical movements in either direction.


Thank @maxim., I just read your comment, I will take some time to dig into it and will come back with my results!

As always, thanks to all!

EDIT

Just to say that I was already able to load the first three upper-left rows with an ugly code that I am going to improve, but at least I think I understood what I have to do now ;)
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sat Nov 05, 2022 11:29 pm
One thing that is driving me crazy: how do I declare an offset?

I want to do what @maxim said: draw the top left area of my background (which is 96x28 tiles, also attached).

To draw the first line, I do:

ld hl,$3800 | VRAMWrite
call SetVDPAddress
ld hl,TileMapData ; my 96x28 tiles bg
ld bc,64
call CopyToVDP


The problem I am trying to solve is writing all other 27 lines below.

I know I have to do something like this, but I am struggling to find a way to do it dynamically (not manually typing all the values line by line:

; SECOND LINE
ld hl,$3800+64 | VRAMWrite
ld hl,TileMapData+192
ld bc,64
call CopyToVDP

; THIRD LINE
ld hl,$3800+128 | VRAMWrite
ld hl,TileMapData+384
ld bc,64
call CopyToVDP


I tried defiing two labels and setting an initial value like this:

TileMapIndex equ $c105
TileMapOffset equ $c106

ld a,192                           ; (96 tiles width = 192 bytes)
ld (TileMapIndex),a
ld a,64                            ; virtual screen width in bytes (32x2)
ld (TileMapOffset),a


Then in a rept 27 I modify these like so:

rept 27                             ; repeat 27 times (screen height in bytes (28x2) - first line)
        ld hl,$3800+TileMapOffset | VRAMWrite
        call SetVDPAddress
        ; 2. Output tilemap data line by line
        ld hl,TileMapData+TileMapIndex
        ld bc,64                        ; Counter for number of bytes to write
        call CopyToVDP

        ; increment Tile Map index and offset
        ld hl,(TileMapIndex)
        ld bc,192
        adc hl,bc
        ld (TileMapIndex),hl

        ld hl,(TileMapOffset)
        ld bc,64
        adc hl,bc
        ld (TileMapOffset),hl
       
    endr


However, the problem is, I cannot seem to be able to replicate the same manual code this way (the result is always a mess).

I am pretty sure I am on the right track but I am certainly doing a super noobie mistake here.

Keep trying...
tilemap.txt (15.94 KB)

  View user's profile Send private message
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14225
  • Location: London
Reply with quote
Post Posted: Sun Nov 06, 2022 8:32 am
I made a similar mistake at first, which is to try to use assembler maths instead of Z80 maths. Unfortunately you have to do the maths at runtime so you are wasting time trying to make it do the right thing this way.

Since you want to do scrolling, ultimately you need to have code at runtime that draws either an entire row or column of data to the tilemap in the right place. Drawing an entire screen is just a repetition of that and indeed many games use scrolling to fill in the tilemap before turning on the screen. So let’s say we want to draw a single row.

Let’s also say we know the coordinates in tilemap space of the left side of the row we want to draw (i.e. 0,0 for the top left; 1,0 for 8px to the right, and so on) are x,y. Therefore to draw a row from there we need to compute TileMapData + (y * TileMapWidth + x) * 2; and then copy 64 bytes from there to the correct place in video memory. Computing the correct place in video memory is also tricky because it depends on how the screen is scrolled, but at first try for the start of the tilemap and then once your “load a row” code is working you can change things later.

As the Z80 doesn’t support multiplication, you either need to use a multiplication routine that does it using shifting and adding, or if your level map is a power of 2 wide you can do the necessary work using shifts. All this is quite tricky.
  View user's profile Send private message Visit poster's website
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14225
  • Location: London
Reply with quote
Post Posted: Sun Nov 06, 2022 9:01 am
…if you only want horizontal scrolling then you need to make a “draw a column” function. That’s a bit harder.
  View user's profile Send private message Visit poster's website
  • Joined: 25 Feb 2006
  • Posts: 760
  • Location: Belo Horizonte, MG, Brazil
Reply with quote
Post Posted: Sun Nov 06, 2022 10:18 am
Not sure if this will help, but there is this very old experiment of mine:
https://www.smspower.org/forums/9600-SMSPlatformGameEngine

It is in C, made for a very old version of Z88DK (before they had decent ROM support) but its horizontal scrolling is pretty solid, even if its multidirectional scrolling is glitchy. The horizontal scrolling functionality is implemented by drawing columns of tiles as required. Maybe it could help understanding how the scrolling can be implemented.
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sun Nov 06, 2022 2:17 pm
Thank you both for the super useful feedbacks!

I will try to come back to the code soon and try to implement your suggestions.

I will definitely go with defining both a drawRow and a drawColumn functions, as ideally I would need both (the idea is an (mostly) horizontal platform game like Sonic, but that can also scroll vertically as a way to explore below and above areas). The idea of having these functions to also draw the entire screen through scrolling before turning on the screen is also super clever, I like it!

@haroldoop: I will surely have a look at your code, thanks for sharing ;)
  View user's profile Send private message
  • Joined: 05 Sep 2013
  • Posts: 3259
  • Location: Torino, then London, now Stockholm
Reply with quote
Post Posted: Sun Nov 06, 2022 4:58 pm
if you write code that can draw a new column from your map, writing code that draws a new line instead should be much simpler

anyway the approach to draw a new column is to calculate the VRAM address of the top most tile in the column you want to load and have a loop that goes through the 28 tiles in the column to update them all

of course you also need to calculate the address of the source data, and move that address in the proper way while copying data

a simplified pseudocode could be

- calculate target address for topmost tile, Nth column
- calculate source address for the topmost tile to be transferred
*** set VDP address to the desired destination
- copy two bytes from the map source address to VRAM
- if 28 tiles copied, we're done
- add 64 to the target address (the next row's tile address is always 64 bytes away)
- add map's width ×2 (again, a constant) to the source address (assuming your map is a row-major matrix)
- loop to ***
  View user's profile Send private message Visit poster's website
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Mon Nov 07, 2022 7:27 am
sverx wrote
- calculate target address for topmost tile, Nth column
- calculate source address for the topmost tile to be transferred
*** set VDP address to the desired destination

And could you track these addresses as you load new columns too?

i.e. something like

Init:
- NextColumnSrc := <initial value>
- NextColumnDst := <initial value>

LoadNextColumn:
- (do sverx's p-code, using NextColumnSrc and NextColumnDst as starting values)
- if not at end of map:
  - NextColumnSrc := NextColumnSrc + 2
  - NextColumnDst := (NextColumnDst +1) % 64
- endif
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Mon Nov 07, 2022 8:02 am
Indeed, my idea would be to track all the indexes so that the arguments of my drawRow/drawCol functions would be these addresses and I would always know what to draw next when scrolling happens (and depending on which direction).

Of course, this is in theory, I would need to actually code this, but thanks for the many suggestions!

I think my initial idea and the code I provided (although not working) was not to that far from the suggestions, but of course now I have plenty of new an better ideas to work on :)

Will post my progress as I have any update. As always, thanks!
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sun Nov 13, 2022 10:52 pm
Hi everyone,

I had the time for a first attempt on implementing a DrawColumn function, and thought I'd post my progress so far (I warn you,it's very ugly).

The first two tiles of the column are drawn correctly, but unfortunately after those the result gets messy. I am looking at the values of my labels via debugging and I do see strange values there that I will correct.

I guess I am not doing the addition correctly (probably due to the 16-bit nature of the calculation or the "endiannes" of the values, I am still figuring out).

Anyway, here is my first draft of DrawColumn function:

; Various labels before and after these
NextColumnSrc equ $c105 ; tilemap col address
NextColumnDst equ $c106 ; VRAM col address
TileMapVRAM equ $c107   ; TileMap address
TileMapWidth equ $c0    ; TileMap WIDTH x2

; ...

ld hl,0                  ; initial tile source address
ld (NextColumnSrc),hl    ; set source column

ld hl,0                  ; initial tile destination shift in VRAM
ld (NextColumnDst),hl    ; set destination column

ld hl,$3800
ld (TileMapVRAM),hl

ld de,28                 ; go through the 28 tiles in the column
call DrawColumn

; ...

DrawColumn:
        ld hl,(TileMapVRAM)
        ld bc,(NextColumnDst)
        add hl,bc
        ld (TileMapVRAM),hl
        ld bc,VRAMWrite
        add hl,bc
        call SetVDPAddress
        ; 2. Output tile data
        ld hl,TileMapData                 ; Location of tile data
        ld bc,2                           ; Counter for number of bytes to write
        call CopyToVDP
       
        ld bc,$40                         ; next row's tile address is always 64 bytes away
        ld hl,(NextColumnDst)
        add hl,bc                         ; add 64 to the target address
        ld (NextColumnDst),hl             ; store new VRAM col address

        ld bc,TileMapWidth                ; assuming map is a row-major matrix
        ld hl,(NextColumnSrc)
        add hl,bc                         ; add map's width ×2 (again, a constant) to the source address
        ld (NextColumnSrc),hl             ; store new VRAM col address

        dec de
        ld a,e
        or d
        jr nz,DrawColumn

        ret


EDIT
@willbritton I know NextColumnSrc and NextColumnDst are not what you meant, it's just I understood it later and I will use them to save the values you had in mind instead of what I am doing now (my labels' names are still confusing, I will need to refactor them soon)
  View user's profile Send private message
  • Joined: 05 Sep 2013
  • Posts: 3259
  • Location: Torino, then London, now Stockholm
Reply with quote
Post Posted: Mon Nov 14, 2022 9:30 am
if you're using WLA-DX, do yourself a favor and switch from these:

NextColumnSrc equ $c105 ; tilemap col address
NextColumnDst equ $c106 ; VRAM col address
TileMapVRAM equ $c107   ; TileMap address

to a ramsection - it's much better.
  View user's profile Send private message Visit poster's website
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Mon Nov 14, 2022 12:09 pm
Looks to me like you're copying from the same source address (`TileMapData`) every time around the loop.

You are updating the value of `NextColumnSrc` but never actually using it.
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Mon Nov 14, 2022 1:16 pm
willbritton wrote
Looks to me like you're copying from the same source address (`TileMapData`) every time around the loop.

You are updating the value of `NextColumnSrc` but never actually using it.


True! I guess I was not so sober yersterday night when I did this :)

BTW, here is the updated code (still not working, but just for the records):

; Various labels before and after these
NextColumnSrc equ $c105 ; tilemap col address
NextColumnDst equ $c106 ; VRAM col address
TileMapVRAM equ $c107   ; TileMap address
TileMapWidth equ $c0    ; TileMap WIDTH x2

; ...

ld hl,0                  ; initial tile source address
ld (NextColumnSrc),hl    ; set source column

ld hl,0                  ; initial tile destination shift in VRAM
ld (NextColumnDst),hl    ; set destination column

ld hl,$3800
ld (TileMapVRAM),hl

ld de,28                 ; go through the 28 tiles in the column
call DrawColumn

; ...

DrawColumn:
        ld hl,(TileMapVRAM)
        ld bc,(NextColumnDst)
        add hl,bc
        ld (TileMapVRAM),hl
        ld bc,VRAMWrite
        add hl,bc
        call SetVDPAddress
        ; 2. Output tile data
        ld hl,TileMapData                 ; Location of tile data
        ld bc,(NextColumnSrc)
        add hl,bc
        ld bc,2                           ; Counter for number of bytes to write
        call CopyToVDP
       
        ld bc,$40                         ; next row's tile address is always 64 bytes away
        ld hl,(NextColumnDst)
        add hl,bc                         ; add 64 to the target address
        ld (NextColumnDst),hl             ; store new VRAM col address

        ld bc,TileMapWidth                ; assuming map is a row-major matrix
        ld hl,(NextColumnSrc)
        add hl,bc                         ; add map's width ×2 (again, a constant) to the source address
        ld (NextColumnSrc),hl             ; store new VRAM col address

        dec de
        ld a,e
        or d
        jr nz,DrawColumn

        ret
  View user's profile Send private message
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Mon Nov 14, 2022 1:40 pm
umbe1987 wrote

        ld bc,TileMapWidth                ; assuming map is a row-major matrix
        ld hl,(NextColumnSrc)
        add hl,bc                         ; add map's width ×2 (again, a constant) to the source address
        ld (NextColumnSrc),hl             ; store new VRAM col address


You mention a x2 but where are you actually doing it? Does `TileMapWidth` contain the width of the source screen map in tiles, or in bytes?

If it's the width in tiles then you need to do a (16 bit) multiply by 2 here, otherwise if `TileMapWidth` contains the width in bytes you should be okay, presuming that your source contains uncompressed asset data ready to be copied direct to the VDP.

EDIT: I see you say, above:
TileMapWidth equ $c0    ; TileMap WIDTH x2


Although are you really saying the source screen map is 192 bytes wide? That doesn't sound like enough - only 96 tiles?
  View user's profile Send private message Visit poster's website
  • Joined: 23 Jan 2010
  • Posts: 210
Reply with quote
Post Posted: Mon Nov 14, 2022 2:35 pm
Quote
BTW, here is the updated code (still not working, but just for the records):

Everybody helping you is welcome. I think that a member with great knowledge about horizontal scrolling is @eruiz but i dont know if he work with assembly or C, i guess that is last case.
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Mon Nov 14, 2022 7:12 pm
willbritton wrote
umbe1987 wrote

        ld bc,TileMapWidth                ; assuming map is a row-major matrix
        ld hl,(NextColumnSrc)
        add hl,bc                         ; add map's width ×2 (again, a constant) to the source address
        ld (NextColumnSrc),hl             ; store new VRAM col address


You mention a x2 but where are you actually doing it? Does `TileMapWidth` contain the width of the source screen map in tiles, or in bytes?

If it's the width in tiles then you need to do a (16 bit) multiply by 2 here, otherwise if `TileMapWidth` contains the width in bytes you should be okay, presuming that your source contains uncompressed asset data ready to be copied direct to the VDP.

EDIT: I see you say, above:
TileMapWidth equ $c0    ; TileMap WIDTH x2


Although are you really saying the source screen map is 192 bytes wide? That doesn't sound like enough - only 96 tiles?


Thank you for the message. The source tilemap I am using is made of 96x28 tiles, and it's raw data (I have attached it in one of my previous messages in txt format (file is called "tilemap.txt"), I modified the format so that I could attach it, but the original one is .inc and it's the same). I think this makes it 192 bytes wide but I might well be wrong.

I hope to have time to come back to the code soon and have another round of debugging: from what I saw last time, the values did not seem to increase the way I intended at each iteration and I surely need to investigate better on the causes.

BTW, my code is available at https://github.com/umbe1987/cinos but I haven't push these last modifications since they don't work, and the assets are probably a bit different than what I am using and sharing here now (will update the code when I find a solution).
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Tue Nov 15, 2022 1:47 pm
Quick update.

I have a drawColumn function working properly now! It was hell hard, but I finally got it! I am still not pushing this change to my github repo since Iam only drawing one column at the moment, and was wondering on the alternatives to draw the entire screen.

One obvious way is by repeating the same drawColumn function by the width of the screen.

However, I was focusing on the words of @maxim and understand whether I should be doing this using scrolling. In the end, I will need this to make new tiles appear as my player moves in the game, so I thought better to start thinking about this now.

The updated and working drawColumn is below:

; other labels before
NextRowSrc equ $c105    ; store tilemap source row address (2 bytes)
NextColSrc equ $c107    ; store tilemap source col address (2 bytes)
NextRowDst equ $c109    ; store tilemap row address in VRAM (2 bytes)
NextColDst equ $c10b    ; store tilemap col address in VRAM (2 bytes)
TileMapWidth equ $60    ; TileMap WIDTH
TileMapHeight equ $1c   ; TileMap HEIGHT

; ...

ld hl,0                  ; initial tile source address
ld (NextRowSrc),hl       ; set source column

ld hl,$3800
ld (NextRowDst),hl

call DrawColumn

; ...

DrawColumn:
    ld de,TileMapHeight               ; go through all rows in the tilemap
    DrawColumnLoop:
        ; Set VRAM write address to tilemap destination index
        ld hl,(NextRowDst)
        ld a,l
        out (VDPControl),a
        ld a,h
        or a,$40
        out (VDPControl),a
        ; 2. Output tilemap data
        ld hl,TileMapData                 ; Location of tile data
        ld bc,(NextRowSrc)
        add hl,bc
        ld bc,2                           ; Counter for number of bytes to write
        call CopyToVDP
       
        ld bc,$40                         ; next row's tile address is always 64 bytes away
        ld hl,(NextRowDst)
        add hl,bc                         ; add 64 to the target address
        ld (NextRowDst),hl                ; store new VRAM row address

        ld hl,TileMapWidth                ; assuming map is a row-major matrix
        add hl,hl                         ; double tilemap width as tiles are made of 2 bytes (or 1 word)
        ld bc,(NextRowSrc)
        add hl,bc                         ; add map's width ×2 (again, a constant) to the source address
        ld (NextRowSrc),hl                ; store new tilemap source row address

        dec de
        ld a,e
        or d
        jr nz,DrawColumnLoop

    ret
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Tue Nov 15, 2022 2:17 pm
Sorry for the many posts, feel free to ignore them of course and I hope they don't bother much.

I finally tried optoin 1 (repeating DrawColumn 32 times to fill the screen) to see if that works, and it does.

I pushed the updated code to my repository here: https://github.com/umbe1987/cinos/blob/master/main.asm

For now I am quite happy with the result but I acknowledge big changes should be made to make it work for the rest of the out-of-screen tiles in the source tilemap to load.

Thanks for all the great support and suggestions!
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sat Nov 26, 2022 10:32 am
Thinking on the next steps, I have a few doubts.

I don't expect you to give feedback to each of these points, but I would be happy just to discuss some or even one of them.

Before I turn on the screen, I fill it with the first (left most) 32 columns of my source image (whose size is 96x28).

Imagine the player wants to move right. The background needs to scroll left. Before scrolling, I would need to load the next column on the right (the 33rd column of the source image), otherwise the scroll will make the background wrap and the first column would be shown instead.

My first doubt: to load this column can I use the address I store in "NextColDst" (which would be 33) and call "DrawColumn" using 32 as my my "NextColDst"?

The second doubt: shall I unload the columns which are far from the player position (like the first one )? I am afraid if I don't do this I will load too much tiles into VRAM and run out of memory. Of course, if in the previous point the answer is to overwrite columns with drawColumn instead of just adding them the problem should be solved.

Third and last doubt: I think the scroll works in pixels depending on the value of the H scroll register (I increase/decrease it with a constant "hspeed" variable, which I set to 3). The tiles are 8x8 pixels. I think that I am not supposed to load new columns each time the player moves, because the speed of the scroll might be less than the width of a tile. Am I supposed to keep track also of the X position of the player and check if it passed a multiple of 8 and only in that case load a new column?
  View user's profile Send private message
  • Site Admin
  • Joined: 19 Oct 1999
  • Posts: 14225
  • Location: London
Reply with quote
Post Posted: Sat Nov 26, 2022 11:48 am
Last edited by Maxim on Sat Nov 26, 2022 4:08 pm; edited 1 time in total
There are only 32 columns in video memory. There is a setting to make the screen hide the left side to avoid showing the wrong data when scrolling. So when it’s time to show column 32 (counting from 0), you draw it in column 0.
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sat Nov 26, 2022 12:00 pm
This is great, thank you @maxim, will definitely use this trick! Just need to figure out how to enable this setting but I am quite confident I will make it ;)
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sat Nov 26, 2022 4:14 pm
kusfo wrote
There's a really useful register to hide the left-most column in the screen, so you can update it without being apparent.


This is what also @kusfo suggested almost two months ago but I guess it was too early for me to understand :) Now I get it, thanks to you too (super late)!
  View user's profile Send private message
  • Joined: 29 Mar 2012
  • Posts: 774
  • Location: Spain
Reply with quote
Post Posted: Sun Nov 27, 2022 8:32 am
umbe1987 wrote
kusfo wrote
There's a really useful register to hide the left-most column in the screen, so you can update it without being apparent.


This is what also @kusfo suggested almost two months ago but I guess it was too early for me to understand :) Now I get it, thanks to you too (super late)!


you're welcome!
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Sun Nov 27, 2022 3:54 pm
Last edited by umbe1987 on Sun Nov 27, 2022 7:10 pm; edited 1 time in total
Hello everyone. A quick update:

I tried to implement the suggestions but still failed to implement the scroll correctly (I think I am very close though).

I was able to hide the left column with:

ld a,%00100110
out (VDPControl),a
ld a,$80
out (VDPControl),a


Then, I reset the destination column to 0 so that my "drawColumn" function can draw the next column of my source image in column 0 (I hope this is what @maxim suggested, but I am not entirely sure).

; update blank left column in tile destination
ld hl,0
ld (NextColDst),hl
call DrawColumn          ; update blank left column


After this, I scroll (only when moving right for now, just to see if things work).

ld a,(scroll)        ; Scroll background - update the horizontal scroll buffer.
sub hspeed           ; sub constant speed to scroll
ld (scroll),a        ; update scroll buffer.

ld b,HScrollReg      ; make the scroll happening
call SetRegister


Strangely, in Emulicious I see the first column of the TileMap Viewer gets updated with the correct data, however, in the screen I see a unique column moving and updating while scrolling.

Here is a short video to give you the idea. What could be wrong?
hscroll_glitch.mp4 (689.13 KB)


  View user's profile Send private message
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Sun Nov 27, 2022 4:26 pm
You have to remember that your screen coordinates are changing all the time as you scroll.

To start with, with 0 horizontal scroll, column 0 is the left most column on screen, but as soon as you scroll the background 8 pixels to the left it's column 1 which is the left most column, etc.

So when you bring in your next column on the left hand side, you need to put it in a different place in VRAM depending on your coarse scroll position.

But you are nearly there - keep going!
  View user's profile Send private message Visit poster's website
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Tue Nov 29, 2022 8:34 pm
willbritton wrote
You have to remember that your screen coordinates are changing all the time as you scroll.

To start with, with 0 horizontal scroll, column 0 is the left most column on screen, but as soon as you scroll the background 8 pixels to the left it's column 1 which is the left most column, etc.

So when you bring in your next column on the left hand side, you need to put it in a different place in VRAM depending on your coarse scroll position.

But you are nearly there - keep going!


Thank you as always @willbritton!

A quick update: I was able to make the scroll working when moving towards the right at a constant "hspeed" of 8 (pixels)!!!

I had to check that "nextColDst" was not greater than 64 (double the screen width) and if so resetting it to 0 (so now "nextColDst" can only be 0-64).

CheckEndMap:
; reset nextColSrc if we are at the end of the map
    ld hl,TileMapWidth
    add hl,hl           ; double tilemap width as tiles are made of 2 bytes (or 1 word)
    ld bc,(nextColSrc)
    sbc hl,bc
    jr nz,CheckEndScreen
    ld hl,0
    ld (nextColSrc),hl

CheckEndScreen:
; reset nextColDst if we are at the end of the screen
    ld a,ScreenWidth
    add a
    ld hl,(NextColDst)
    ld b,l
    sub b
    jr nz,MovePlayerRight
    ld hl,0
    ld (NextColDst),a


Next steps to have a first proper horizontal scrolling would be IMHO (more or less):

- changing the "drawColumn" so that in case of a left movement "nextColSrc" and "nextcolDst" will decrease instead of increase.
- allow for "hspeed" other than 8 by checking if the cumulated "hspeed" at each loop is greater than 8. Only then call the "drawColumn" and reset the cumulated speed

Again, thanks for the precious suggestions, I think I am almost there.
  View user's profile Send private message
  • Joined: 23 Mar 2013
  • Posts: 609
  • Location: Copenhagen, Denmark
Reply with quote
Post Posted: Wed Nov 30, 2022 6:48 am
umbe1987 wrote
[Next steps to have a first proper horizontal scrolling would be IMHO (more or less):
- allow for "hspeed" other than 8 by checking if the cumulated "hspeed" at each loop is greater than 8. Only then call the "drawColumn" and reset the cumulated speed
Again, thanks for the precious suggestions, I think I am almost there.

Great progress! I have only worked with horizontally scrolling new columns into the screen. I usually keep some variables for horizontal scroll control: fineScroll is a value between 0-7, for keeping track of pixel level scrolling within a column, and screenScroll (or similar name) for keeping track of the value in the VDP h-scroll register, and nextColumn (or currentColumn) for where to load the column on the name table/tile map in the VDP, and mapHead for keeping track of column level scrolling within the map.

Every time the screen scrolls, screenScroll gets updated and loaded into the VDP register. fineScroll is updated as well. When fineScroll goes outside the 0-7 range (the “cumulated hspeed” part in your post) time to read a column from the map head, and load it into the correct place on the name table.

Above text is just some thoughts - you seem to be on the right track!
  View user's profile Send private message Visit poster's website
  • Joined: 06 Mar 2022
  • Posts: 213
  • Location: London, UK
Reply with quote
Post Posted: Wed Nov 30, 2022 9:57 am
Further to hang-on's comments, it may be helpful to consider that the relationship between the "fine scroll" value and the horizontal scroll register is simply that the fine scroll is the bottom 3 bits of the scroll register.

So you might be able to do something like


; update horizontal scroll register value and store in a, then do
and $f8 ; = %11111000
ld hl, CurrentScreenCol
cp (hl)
jr nz, LoadNewColumn


Assuming that CurrentScreenCol contains a copy of the scroll register value with the bottom bits all 0. There are of course many other variations!

Also, hard to tell from your snippets whether it would work, but you might find you can simplify your check to see if your column has rolled around the screen.

In general, you should be able to implement a modulo operation on a power of 2, like 64, with a simple bitwise and, e.g.


ld a, (NumberPossiblyBiggerThan64)
and 63


Because 63 is %00111111, it means that if a was greater than 63 the result of the and operation will mean it can never be as high as 64 - it will seem equivalent to subtracting 64 as I think you are effectively doing in your CheckEndScreen routine. Also I think your routine might not work so well if you are moving very fast since it will always reset the column to 0 rather than n-64.
  View user's profile Send private message Visit poster's website
  • Joined: 23 Jan 2010
  • Posts: 210
Reply with quote
Post Posted: Wed Nov 30, 2022 12:57 pm
I dont know if it will help you but i will post a link for MonkeylLad homebrew:
https://www.smspower.org/Homebrew/MonkeyLad-SMS
Here video:


You can verify that is an homebrew with horizontal scrolling working.
The link above will point for game and source code. Maybe you can compare your code with it and achieve some progress.
God bless you.
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Wed Nov 30, 2022 9:34 pm
That is a really nice looking platformer with very nice graphics and smooth scrolling!

Thank you @segarule, I'll definitely check the sources ;)
  View user's profile Send private message
  • Joined: 03 Dec 2021
  • Posts: 42
Reply with quote
Post Posted: Yesterday at 3:46 pm
@willbritton @hang-on

These are really great suggestions, sorry I did not notice and reply to your comments earlier.

willbritton wrote
Further to hang-on's comments, it may be helpful to consider that the relationship between the "fine scroll" value and the horizontal scroll register is simply that the fine scroll is the bottom 3 bits of the scroll register.


This is great to know, one less variable to my (already long) list ;)

Hopefully I will have time to finish implementing the horizontal scrolling soon.
  View user's profile Send private message
Reply to topic



Back to the top of this page

Back to SMS Power!