Jump to content
karri

Pong tutorial

Recommended Posts

They had a Finnish Championship in programming recently and the task was to program Pong.

 

Actually Pong is one of the first games programmed for graphical computers. When I was a kid you could see Pong videogames at service stations and play a game for 20 pennies.

 

Here is a screenshot of the Lynx Pong:

 

post-2099-0-24275400-1353922430_thumb.png

 

So how does the code for this look like.

 

Include some libraries:

#include <lynx.h>
#include <tgi.h>
#include <6502.h>
#include <joystick.h>
#include <stdlib.h>

 

Create a literal sprite for a paddle:

unsigned char paddle[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};

 

Create a literal sprite for the ball:

unsigned char ball[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};

 

Create a literal sprite of a wall element that we can stretch later:

unsigned char wall[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};

 

Then we create a widget object to hold out paddle or ball. To this widget object we add two extra fields: horizontal velocity and vertical velocity. This allows the sprite to have a property of speed.

 

typedef struct {
 char collindex;
 SCB_REHV sprite;
 PENPAL_1;
 signed char hvel;
 signed char vvel;
} sprite_t;

 

Then we need to define the sprite stucture for our paddles:

 

sprite_t LeftPaddle = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   1,
   0,
   paddle,
   10, 73, 256, 256
 },
 {COLOR_GREY},
 0, 0
};

sprite_t RightPaddle = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   2,
   0,
   paddle,
   159-10-6, 70, 256, 256
 },
 {COLOR_GREY},
 0, 0
};

 

We also need to have walls from which the ball can bounce:

 

sprite_t TopWall = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   3,
   0,
   wall,
   -24, 1, 160 / 6 * 256, 256
 },
 {COLOR_GREEN},
 0, 0
};

sprite_t BottomWall = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   4,
   0,
   wall,
   -24, 101-4, 160 / 6 * 256, 256
 },
 {COLOR_GREEN},
 0, 0
};

 

Last but not least we need a ball:

 

sprite_t Ball = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   5,
   0,
   ball,
   10, 10, 256, 256
 },
 {COLOR_GREY},
 2, 1
};

 

Please note that now all our widgets have id's also. I gave a collision index to every element while I created them.

 

LeftPaddle = 1

RightPaddle = 2

TopWall = 3

BottomWall = 4

Ball = 5

 

We need these later on to know who collided with what.

 

Then we set up some score counters:

 

signed RightScore = 0;
signed LeftScore = 0;

 

Now it is time to start writing the program itself. We start with defining the main:

 

void main()
{
 unsigned char joy;
 char buffer[16];

 

Here I also defines a char for holding the joypad and a buffer for integer to ascii conversions later in the game.

 

The we need to initialize the chips:

 

joy_install(&lynxjoy);
tgi_install(&lynxtgi);
tgi_init();
tgi_setcollisiondetection(1);
CLI();

 

Now we get to the main loop that runs forever. (In the real pong we usually stop when either player reaches 15 points).

 

while (1) {

 

Then we erase the collision buffers and set the paddles grey.

 

while (tgi_busy())
 ;
/* Erase collision buffer */
tgi_clear();
LeftPaddle.penpal[0] = COLOR_GREY;
RightPaddle.penpal[0] = COLOR_GREY;

 

Now we check if the players press any buttons (up/down or A/B):

 

/* Move paddles */
joy = joy_read(JOY_1);
if (JOY_BTN_UP(joy)) {
 LeftPaddle.sprite.vpos -= 1;
}
if (JOY_BTN_DOWN(joy)) {
 LeftPaddle.sprite.vpos += 1;
}
if (JOY_BTN_FIRE(joy)) {
 RightPaddle.sprite.vpos -= 1;
}
if (JOY_BTN_FIRE2(joy)) {
 RightPaddle.sprite.vpos += 1;
}

 

Then we let the ball move and add score if the ball exits the screen.

 

Ball.sprite.hpos += Ball.hvel;
if (Ball.sprite.hpos < 3) {
 RightScore++;
 Ball.sprite.hpos = RightPaddle.sprite.hpos;
 Ball.sprite.vpos = RightPaddle.sprite.vpos + 10;
}
if (Ball.sprite.hpos > 160 -3) {
 LeftScore++;
 Ball.sprite.hpos = LeftPaddle.sprite.hpos;
 Ball.sprite.vpos = LeftPaddle.sprite.vpos + 10;
}
Ball.sprite.vpos += Ball.vvel;

 

Then we paint out walls and paddles and last our ball to see if it collides.

 

/* Paint stationary sprites */
tgi_sprite(&TopWall.sprite);
tgi_sprite(&BottomWall.sprite);
tgi_sprite(&LeftPaddle.sprite);
tgi_sprite(&RightPaddle.sprite);

/* Draw last collidable sprite */
tgi_sprite(&Ball.sprite);

 

After the collision has occurred we can see who collided with what.

 

/* Examine collisions */
switch (Ball.collindex) {
case 0:
 break;
case 1:
 Ball.hvel = 2;
 LeftPaddle.penpal[0] = COLOR_RED;
 tgi_sprite(&LeftPaddle.sprite);
 break;
case 2:
 Ball.hvel = -2;
 RightPaddle.penpal[0] = COLOR_RED;
 tgi_sprite(&RightPaddle.sprite);
 break;
case 3:
case 4:
 Ball.vvel = -Ball.vvel;
 break;
default:
 break;
}

 

Then we print the score for this round and update the display.

 

tgi_setcolor(COLOR_WHITE);
itoa(LeftScore, &buffer[0], 10);
tgi_outtextxy(40, 10, buffer);
itoa(RightScore, &buffer[0], 10);
tgi_outtextxy(120, 10, buffer);
tgi_updatedisplay();
}
}

 

That's it. To compile it we can run the codes manually or use a Makefile.

 

Here is a Makefile

RM=rm -f
CO=co65
CC=cc65
AS=ca65
CL=cl65

ifeq ($(CC65_HOME),)
 CC65_HOME=/usr/local/lib/cc65
endif
ifeq ($(CC65_INC),)
 CC65_INC=$(CC65_HOME)/include
endif
ifeq ($(CC65_ASMINC),)
 CC65_ASMINC=$(CC65_HOME)/asminc
endif

all:
 $(CP) $(CC65_HOME)/tgi/lynx-160-102-16.tgi .
 $(CP) $(CC65_HOME)/joy/lynx-stdjoy.joy .
 $(CO) --code-label _lynxtgi lynx-160-102-16.tgi
 $(CO) --code-label _lynxjoy lynx-stdjoy.joy
 $(AS) -t lynx lynx-160-102-16.s
 $(AS) -t lynx lynx-stdjoy.s
 $(CC) -t lynx pong.c
 $(AS) -t lynx pong.s
 $(CL) -t lynx -o pong.lnx -m pong.map pong.o lynx-160-102-16.o lynx-stdjoy.o lynx.lib

clean:
 $(RM) pong.lnx
 $(RM) pong.s
 $(RM) pong.o
 $(RM) lynx-stdjoy.s
 $(RM) lynx-stdjoy.o
 $(RM) lynx-stdjoy.joy
 $(RM) lynx-160-102-16.s
 $(RM) lynx-160-102-16.o
 $(RM) lynx-160-102-16.tgi

 

To use the Makefile you simply type "make".

 

For lazy people you can download everything here.

pong.zip

 

Enjoy,

 

Karri

Edited by karri
  • Like 3

Share this post


Link to post
Share on other sites

Wow, this just goes to show how powerful the Lynx graphics engine is. Thanks for the tutorial in this.

Quick question: any particular reason to have the sprites not stretched in the vertical direction? You could do with just two one-pixel definitions and stretch them horizontally and vertically.

Share this post


Link to post
Share on other sites

Wow, this just goes to show how powerful the Lynx graphics engine is. Thanks for the tutorial in this.

Quick question: any particular reason to have the sprites not stretched in the vertical direction? You could do with just two one-pixel definitions and stretch them horizontally and vertically.

 

True. The graphics engine is very modern and powerful.

 

To tell you the truth I did not think about stretching a pixel :( That would have made the code a lot smaller. Now I have 6 pixel wide bars with no stretching.

Share this post


Link to post
Share on other sites

I found a terrible, horrible bug in my Pong game. The C-stack and the collision buffer share the same memory area. This means that the content of he stack is filled with zeros on every round.

 

Whenever you compile for collision detection you must use the proper configuration file.

 

The Magic line in the makefile should be:

 

$(CL) -t lynx -C lynx-coll.cfg -o pong.lnx -m pong.map pong.o lynx-160-102-16.o lynx-stdjoy.o lynx.lib

 

There is a built-in configuration called lynx-coll.cfg that puts the stack lower and reserves the memory for the collision buffer.

 

Here is the corrected code.

 

pong.zip

Share this post


Link to post
Share on other sites

Today I noticed that cc65 has just got a new feature - static drivers!!!

 

So the new way to build things is becoming really simple now.

 

There is no longer a need to do tricks with the joypad driver or the tgi driver. The new code looks like this:

 

#include <lynx.h>
#include <tgi.h>
#include <6502.h>
#include <joystick.h>
#include <stdlib.h>

unsigned char paddle[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};

unsigned char ball[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};

unsigned char wall[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};
unsigned char wall[] = {
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 2, 0x7E,
 0
};

typedef struct {
 char collindex;
 SCB_REHV sprite;
 PENPAL_1;
 signed char hvel;
 signed char vvel;
} sprite_t;

sprite_t LeftPaddle = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   1,
   0,
   paddle,
   10, 73, 256, 256
 },
 {COLOR_GREY},
 0, 0
};

sprite_t RightPaddle = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   2,
   0,
   paddle,
   159-10-6, 70, 256, 256
 },
 {COLOR_GREY},
 0, 0
};
sprite_t TopWall = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   3,
   0,
   wall,
   -24, 1, 160 / 6 * 256, 256
 },
 {COLOR_GREEN},
 0, 0
};

sprite_t BottomWall = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   4,
   0,
   wall,
   -24, 101-4, 160 / 6 * 256, 256
 },
 {COLOR_GREEN},
 0, 0
};

sprite_t Ball = {
 0,
 {
   BPP_1 | TYPE_NORMAL,
   LITERAL | REHV,
   5,
   0,
   ball,
   10, 10, 256, 256
 },
 {COLOR_GREY},
 2, 1
};

void main()
{
 unsigned char joy;
 char buffer[16];
 signed RightScore = 0;
 signed LeftScore = 0;

 joy_install(&lynx_stdjoy);
 tgi_install(&lynx_160_102_16);
 tgi_init();
 tgi_setcollisiondetection(1);
 CLI();
 while (1) {
   while (tgi_busy())
  ;
   /* Erase collision buffer */
   tgi_clear();
   LeftPaddle.penpal[0] = COLOR_GREY;
   RightPaddle.penpal[0] = COLOR_GREY;

   /* Move paddles */
   joy = joy_read(JOY_1);
   if (JOY_BTN_UP(joy)) {
    LeftPaddle.sprite.vpos -= 1;
   }
   if (JOY_BTN_DOWN(joy)) {
    LeftPaddle.sprite.vpos += 1;
   }
   if (JOY_BTN_FIRE(joy)) {
    RightPaddle.sprite.vpos -= 1;
   }
   if (JOY_BTN_FIRE2(joy)) {
    RightPaddle.sprite.vpos += 1;
   }
   Ball.sprite.hpos += Ball.hvel;
   if (Ball.sprite.hpos < 3) {
    RightScore++;
    Ball.sprite.hpos = RightPaddle.sprite.hpos;
    Ball.sprite.vpos = RightPaddle.sprite.vpos + 10;
   }
   if (Ball.sprite.hpos > 160 -3) {
    LeftScore++;
    Ball.sprite.hpos = LeftPaddle.sprite.hpos;
    Ball.sprite.vpos = LeftPaddle.sprite.vpos + 10;
   }
   Ball.sprite.vpos += Ball.vvel;

   /* Paint stationary sprites */
   tgi_sprite(&TopWall.sprite);
   tgi_sprite(&BottomWall.sprite);
   tgi_sprite(&LeftPaddle.sprite);
   tgi_sprite(&RightPaddle.sprite);

   /* Draw last collidable sprite */
   tgi_sprite(&Ball.sprite);

   /* Examine collisions */
   switch (Ball.collindex) {
   case 0:
  break;
   case 1:
  Ball.hvel = 2;
  LeftPaddle.penpal[0] = COLOR_RED;
  tgi_sprite(&LeftPaddle.sprite);
  break;
   case 2:
  Ball.hvel = -2;
  RightPaddle.penpal[0] = COLOR_RED;
  tgi_sprite(&RightPaddle.sprite);
  break;
   case 3:
   case 4:
  Ball.vvel = -Ball.vvel;
  break;
   default:
  break;
   }
   tgi_setcolor(COLOR_WHITE);
   itoa(LeftScore, &buffer[0], 10);
   tgi_outtextxy(40, 10, buffer);
   itoa(RightScore, &buffer[0], 10);
   tgi_outtextxy(120, 10, buffer);
   tgi_updatedisplay();
 }
}

 

And to build this from the command line:

 

cl65 -t lynx -C lynx-coll.cfg pong.c

 

No need to have any Makefile's anymore.

  • Like 1

Share this post


Link to post
Share on other sites

So what you are saying is that we do no longer need to run the rules for *.tgi files?

From what I saw in your code, you still need to install the drivers right? Is this change backward compatible? In other words: will old makefiles still work against this particular build and change? You can still create your own versions of the drivers? As long as we can still drop down a level of abstraction and take control, this is great.

Other than these questions: good stuff, more stuff pushed into compiler/framework. Less hassle for the dev.

Share this post


Link to post
Share on other sites

The old way still works of course. It is just that as a convenience this work is automatically done for you in the newest version thanks to Oliver. I just happened to see his commits in the svn and it works just fine :)

Share this post


Link to post
Share on other sites

Thank you so much for this, Karri. I'm working on a ROM emulator for the Atari Lynx based around a PIC18F4550 and have been trying to find a ROM image small enough to fit in the 32K of program memory space of the PIC (eventually I want to load ROM images from a USB thumbdrive, also interfaced to the PIC).

 

I have a few questions:

 

1) This is a .LNX file, for emulators. From what I have read, if I strip off the first 48 bytes from the file I will then have a .LYX ROM image that can work with hardware. Is this correct?

2) The numblocks at the start of the file (once stripped) is 0xFF - does this image incorporate your quick boot code?

3) Will this image (once stripped to be a .LYX file) work on actual Lynx hardware?

 

I have managed to get the Pong1K image working - but not the BLL loader or your PONG. I think it is related to image size and there may still be some hardware issues. Your PONG image should help me with my debugging.

 

Thanks!

 

----

 

I've been doing some debugging and this is what I see:

1) It pulls in the 0xFF as the numblock value

2) It pulls in 51 bytes for the first block. Supposedly chews on it a bit for decrypt, etc. This takes about 450ms. That's quite a long time.

3) It pulls in a LOT of bytes from the 'ROM', starting where it left off after the 1st block. I haven't counted the bytes read, but the sequence is 82ms long, with each read taking about 6.67us each, so around 12,300 bytes or so.

4) And that's it. All cart bus activity stops, the screen is blank, no sound.

 

Considering that the PONG.LNX image is only 5600 bytes - it's pulling in almost 3x what it should.

 

----

 

Ah - the block size is 1K - I had to adjust the PIC code to emulate that size ROM. So now - I have your PONG booting! Yay!

 

So - a few more questions:

 

- There's no sound. I this is correct?

- The paddles cannot be moved, no matter which buttons I press. I am using a LYNX I, not a LYNX II. Does that matter?

- The ball is always being served - it isn't waiting for a button press. Is the emulator different from the actual hardware?

 

Again - thanks! I love it when something other than "INSERT GAME" shows up on the screen.

Edited by timothyscarlson

Share this post


Link to post
Share on other sites

a) the LNX files are mostly filled with 0 or FF to fit to 128k or 256k, even so much less is used.

b) you might consider just using the executeables (*.o) files as these are much smaller as ROMs for testing.

you can convert them to a rom image (without header) by the following small program I did for Lynxman's flash cart.

 

http://lynxdev.atari.org/blog/o2lyx_hack.xhtml

Edited by sage

Share this post


Link to post
Share on other sites

Thanks for the tip. I have seen the code for o2lyx before, but hadn't gotten around to really looking at it yet.

 

I figured out my problem with the paddles - I'm not tristating the data bus from the PIC after the code is completely loaded, so it's interfering with the operation of the paddles - I guess I'm stomping on the I/O for the buttons. Tri-stating (holding in reset) the PIC after the game load allows the buttons to control the paddles. I will have to modify the PIC code to release the data bus.

 

I found a larger image - Centipede (PD) - that still fits in the PIC program memory. Stripped the trailing 0xFFs as suggested for it to fit. Not working yet, but I have to adjust the ROM size again in the PIC code. Something else I need to make automatic.

 

And - from looking at the pong code that Karri provided, I don't thing his pong produces sounds. So that answers that question.

 

I'm getting close enough that I am going to start the USB portion of this project.

Share this post


Link to post
Share on other sites

I have a couple of question about the BS93 header - what the heck is it?

 

From what I can tell, it appears to be an emulator only header. It's not just a ROM image, or a ROM image with the LYNX header (for emulators) prepended. The files appear to be raw Lynx code without the ROM processing, but with the BS93 header attached. Is this correct?

 

Where can I find the format for this header, so I can strip it off and then process the image as a ROM? (If this is what the image is - code that is not processed for ROM). I've looked around the web, but all I can find is code that looks for the 'BS93' tag and then flags the image as 'HOMEBREW'.

 

Thanks for your help.

 

BTW: I will need to add an octal buffer and an AND gate to my ROM emulator design. The PIC being on the data bus is really problematic for the Lynx. I can tristate the data port I am using on the PIC, but then I cannot re-enable it for output fast enough to present data to the Lynx when /CEO (or /CE1) goes low. So much for a clean, 1 part solution using the PIC18F4550.

Edited by timothyscarlson

Share this post


Link to post
Share on other sites

Then BS42 header is 42Bastian's initials.

 

    .segment "BLLHDR"
    .word   $0880
    .dbyt   __RAM_START__
    .dbyt   __BSS_LOAD__ - __RAM_START__ + 10
    .byte   $42,$53
    .byte   $39,$33

Share this post


Link to post
Share on other sites

You can compile code in many ways.

 

There are a few pre-defined headers. If you define __BLLHDR__ in the config file of the cc65 then the linker will add this BLLHDR. It is intended for uploading code to the Lynx at run time over ComLynx.

 

Many programs wait for this magic sequence and start the download of the binary. Once the download is complete the Lynx jumps to the start address.

 

This was one of the best ways to test your stuff before flash carts.

Share this post


Link to post
Share on other sites

Then BS42 header is 42Bastian's initials.

 

... and I thought they were mine plus my house nr ;-))

Share this post


Link to post
Share on other sites

The normal output defines a lnx-header that is intended for emulators only. It looks like this:

; This header contains data for emulators like Handy and Mednafen
;
    .import		 __BLOCKSIZE__
    .export		 __EXEHDR__: absolute = 1


; ------------------------------------------------------------------------
; EXE header
    .segment "EXEHDR"
    .byte   'L','Y','N','X'						 ; magic
    .word   __BLOCKSIZE__						   ; bank 0 page size
    .word   __BLOCKSIZE__						   ; bank 1 page size
    .word   1									   ; version number
    .asciiz "Cart name					  "	   ; 32 bytes cart name
    .asciiz "Manufacturer   "					   ; 16 bytes manufacturer
    .byte   0									   ; rotation 1=left
												    ; rotation 2=right
    .byte   0,0,0,0,0							   ; spare

 

After this header the rest of the code is the raw image for the cart.

 

It starts with an encrypted portion, followed by a directory and lots of files,

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...