This is the third of the four installments we have designed for you to program a simple version of the Pong game from scratch using FUSION-C V1.2.
Objective
In the previous article, we worked on the player's paddle, and in the first of the series, we worked on the game ball. In this one, we will place the two previous elements within the game stage to create a Pong-style game prior to the final article, which will be the complete Pong game.
Example of the initial screen where we will find ourselves upon execution fpong.com
Introduction
As this is the third article in the series, we will no longer present the sections in which a C file is divided and we will go straight to the point.
As we mentioned, the game presented here is of the paddle type. The ball will bounce off the walls of the play area, as it did in the first article. However, here we will remove the rebound from the wall where the player's paddle is located, allowing the player to return the ball by hitting it with the paddle.
If the player fails to touch the ball and it passes beyond the player's paddle, the CPU will increase its score by one point.
Therefore, the goal is to touch the ball with the paddle and return it as many times as possible.
Explanatory figure of the bounces of the ball that has made contact with the paddle.
Documentation section
//
// fpong.c
// Fusion-C Pong game example articles
// Here we will learn how put the pad and the ball in a game
// MoltSXalats 2024
//
Preprocessor section
#include "fusion-c/header/msx_fusion.h"
#include "fusion-c/header/vdp_sprites.h"
#include "fusion-c/header/vdp_graph2.h"
#include <stdlib.h>
Type definition section
typedef struct {
char spr; // Sprite ID
char y; // Y destination of the Sprite
char x; // X destination of the Sprite
char pat; // Pattern number to use
char col; // Color to use (Not usable with MSX2's sprites)
} Sprite;
Definition section of function prototypes
void DrawSprite(void);
void BallCal(void);
void GameStart(void);
void FT_Wait(int);
Definition section of global variables
// Direction of the ball
signed char DirX;
signed char DirY;
char PlyScore,CpuScore;
// Definining the Ball and Pad Sprite Structures
Sprite TheBall;
Sprite PlyPad;
const char PatternBall[]={
0b11100000,
0b11100000,
0b11100000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000
};
const char PatternPad[]={
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000
};
// Movement direction control by the cursors positions
const signed char moves[9]={0,-1,0,0,0,1,0,0,0};
Highlight here that, unlike the other two articles, we will define and use two sprites on the screen, the ball and the player's paddle.
Sprite TheBall;
Sprite PlyPad;
Section of the main function and the game loop
void main (void)
{
char joy;
SetColors(15,0,0);
// 256 x 212 pixel
Screen(5);
Sprite8();
SpriteDouble();
KeySound(0);
SetSpritePattern(0, PatternBall,8);
SetSpritePattern(1, PatternPad,8);
// Defining Variables
// Player Pad Sprite initialization
PlyPad.x=10;
PlyPad.y=100;
PlyPad.spr=1;
PlyPad.pat=1;
PlyScore=0;
CpuScore=0;
// Ball Sprite initialization
TheBall.x=128;
TheBall.y=100;
TheBall.spr=0;
TheBall.pat=0;
DirX=1;
DirY=1;
// IF MSX is Turbo-R Switch CPU to Z80 Mode
if(ReadMSXtype()==3) {
ChangeCPU(0);
}
GameStart();
// Main loop
while (Inkey()!=27)
{
// Reading Joystick 0 (Keyboard)
joy=JoystickRead(0);
// Update the Y position of the Player Pad
PlyPad.y+=moves[joy];
BallCal();
DrawSprite();
FT_Wait(200);
}
// Ending program, and return to DOS
Screen(0);
KeySound(1);
Exit(0);
}
Highlight here the initialization of the two sprites we now have on the screen.
// Player Pad Sprite
PlyPad.x=10;
PlyPad.y=100;
PlyPad.spr=1;
PlyPad.pat=1;
// Ball Sprite
TheBall.x=128;
TheBall.y=100;
TheBall.spr=0;
TheBall.pat=0;
For the first time, in this third article, we will make use of the variable that will keep track of the CPU's score. In this case, it will count the times the player fails to return the ball.
// IF MSX is Turbo-R Switch CPU to Z80 Mode
if(ReadMSXtype()==3) {
ChangeCPU(0);
}
If you notice, we have added a control so that if the program is executed on an MSX Turbo R, the CPU is changed to Z80, since it would not be playable with the R800.
while (Inkey()!=27)
{
// Reading Joystick 0 (Keyboard)
joy=JoystickRead(0);
// Update the Y position of the Player Pad
PlyPad.y+=moves[joy];
BallCal();
DrawSprite();
FT_Wait(200);
}
As we can see, the game loop also consists, just like in the previous article, of two main functions: DrawSprite() and FT_Wait().
In DrawSprite(), we will draw the sprites and control their collision. We will also ensure that they remain within the playing area or screen at all times.
In FT_Wait(), we will introduce a delay control in the screen rendering so that everything flows at a controlled speed with a Z80. This can be done with EnableInterrupt() and Halt(), or with IsVsync() controlling the Vblank states, but due to problems with collisions managed by the VDP itself, we have opted for a simple step counter.
User defined functions section
// Print the initial game screen
void GameStart(void)
{
char Temp[5];
PutText((256-20*8)/2,4,"Press SPACE to START",0);
// Initial Positions
PlyPad.x=10;
PlyPad.y=100;
TheBall.x=128;
TheBall.y=100;
DirX*=-1;
DrawSprite();
Itoa(PlyScore,Temp,10);
PutText(10,4,Temp,0);
Itoa(CpuScore,Temp,10);
PutText(235,4,Temp,0);
while (!IsSpace(WaitKey()))
{}
PutText((256-20*8)/2,4," ",0);
}
With this function, we initialize the game screen at the beginning and each time the ball surpasses the player's paddle. It is also used to update the scoreboard with the times the CPU has scored by getting the ball past us.
// Put all sprite on screen
void DrawSprite(void)
{
// Collision Detection
if (SpriteCollision()) {
DirX*=-1;
}
PutSprite(PlyPad.spr,PlyPad.pat,PlyPad.x,PlyPad.y,15);
PutSprite(TheBall.spr,TheBall.pat,TheBall.x,TheBall.y,15);
// Check Ball Outside Game field
if (TheBall.x<10)
{
CpuScore++;
GameStart();
}
// Check PlyPad Outside Game field
if (PlyPad.y<10)
{
PlyPad.y=10;
}
if (PlyPad.y>190)
{
PlyPad.y=190;
}
}
The novelty within this function responsible for drawing the sprites is the collision control between them, specifically in the case of the collision between the ball and the player's paddle. This is achieved using the SpriteCollision() function from the vdp_sprites.h library in FUSION-C. This function returns 1 if a collision between sprites is detected. We take advantage of the detection moment to change the direction of the ball (DirX*=-1).
In this example, we will only work with the collisions provided by the VDP itself. This simplifies the work a lot but has some problems: if it collides again before the direction change is executed, we may end up in a deadlock, and if we had many sprites on the screen, we would also not have information about which sprite collided.
For all the reasons mentioned above, we will dedicate an article solely to box collisions at the end of the series.
// Ball Position
void BallCal(void)
{
TheBall.x+=DirX;
TheBall.y+=DirY;
// Douncing the Ball on the Top and Bottom border
if (TheBall.y>=190 || TheBall.y<5)
DirY*=-1;
// Douncing the Ball on the right border
if (TheBall.x>=245)
DirX*=-1;
}
In this function, we will control the coordinates of the ball's position and ensure that it does not exit the playing field by bouncing off three out of the four walls.
// Wait Routine
void FT_Wait(int cicles) {
unsigned int i;
for(i=0;i<cicles;i++)
{}
}
Finally, the FT_Wait() function that we mentioned earlier has been modified from the previous two articles, as the original approach had issues with collisions managed by the VDP.
Next, we provide the complete code and at the end a link where you can run the example we have worked on online.
//Documentation section
//
// fpong.c
// Fusion-C Pong game example articles
// Here we will learn how put the pad and the ball in a game
// MoltSXalats 2024
//
// Preprocessor section
#include "fusion-c/header/msx_fusion.h"
#include "fusion-c/header/vdp_sprites.h"
#include "fusion-c/header/vdp_graph2.h"
#include <stdlib.h>
// Definition section of macros and symbolic constants
#define TRUE 1
#define FALSE 0
// Type definition section
typedef struct {
char spr; // Sprite ID
char y; // Y destination of the Sprite
char x; // X destination of the Sprite
char pat; // Pattern number to use
char col; // Color to use (Not usable with MSX2's sprites)
} Sprite;
// Definition section of function prototypes
void DrawSprite(void);
void BallCal(void);
void GameStart(void);
void FT_Wait(int);
// Definition section of global variables
// Direction of the ball
signed char DirX;
signed char DirY;
char PlyScore,CpuScore;
// Definining the Ball and Pad Sprite Structures
Sprite TheBall;
Sprite PlyPad;
const char PatternBall[]={
0b11100000,
0b11100000,
0b11100000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000
};
const char PatternPad[]={
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000,
0b11100000
};
// Movement direction control by the cursors positions
const signed char moves[9]={0,-1,0,0,0,1,0,0,0};
// Section of the main function and the game loop
void main (void)
{
char joy;
SetColors(15,0,0);
// 256 x 212 pixel
Screen(5);
Sprite8();
SpriteDouble();
KeySound(0);
SetSpritePattern(0, PatternBall,8);
SetSpritePattern(1, PatternPad,8);
// Defining Variables
// Player Pad Sprite initialization
PlyPad.x=10;
PlyPad.y=100;
PlyPad.spr=1;
PlyPad.pat=1;
PlyScore=0;
CpuScore=0;
// Ball Sprite initialization
TheBall.x=128;
TheBall.y=100;
TheBall.spr=0;
TheBall.pat=0;
DirX=1;
DirY=1;
// IF MSX is Turbo-R Switch CPU to Z80 Mode
if(ReadMSXtype()==3) {
ChangeCPU(0);
}
GameStart();
// Main loop
while (Inkey()!=27)
{
// Reading Joystick 0 (Keyboard)
joy=JoystickRead(0);
// Update the Y position of the Player Pad
PlyPad.y+=moves[joy];
BallCal();
DrawSprite();
FT_Wait(200);
}
// Ending program, and return to DOS
Screen(0);
KeySound(1);
Exit(0);
}
// Print the initial game screen
void GameStart(void)
{
char Temp[5];
PutText((256-20*8)/2,4,"Press SPACE to START",0);
// Initial Positions
PlyPad.x=10;
PlyPad.y=100;
TheBall.x=128;
TheBall.y=100;
DirX*=-1;
DrawSprite();
Itoa(PlyScore,Temp,10);
PutText(10,4,Temp,0);
Itoa(CpuScore,Temp,10);
PutText(235,4,Temp,0);
while (!IsSpace(WaitKey()))
{}
PutText((256-20*8)/2,4," ",0);
}
// Put all sprite on screen
void DrawSprite(void)
{
// Collision Detection
if (SpriteCollision()) {
DirX*=-1;
}
PutSprite(PlyPad.spr,PlyPad.pat,PlyPad.x,PlyPad.y,15);
PutSprite(TheBall.spr,TheBall.pat,TheBall.x,TheBall.y,15);
// Check Ball Outside Game field
if (TheBall.x<10)
{
CpuScore++;
GameStart();
}
// Check PlyPad Outside Game field
if (PlyPad.y<10)
{
PlyPad.y=10;
}
if (PlyPad.y>190)
{
PlyPad.y=190;
}
}
// Ball Position
void BallCal(void)
{
TheBall.x+=DirX;
TheBall.y+=DirY;
// Douncing the Ball on the Top and Bottom border
if (TheBall.y>=190 || TheBall.y<5)
DirY*=-1;
// Douncing the Ball on the right border
if (TheBall.x>=245)
DirX*=-1;
}
// Wait Routine
void FT_Wait(int cicles) {
unsigned int i;
for(i=0;i<cicles;i++)
{}
}
Conclusions
We are almost done completing the series. In the next article, we will add the CPU-controlled paddle and thus complete the version of Pong that we have been working on with FUSION-C.
As we have discussed in this article, we will dedicate a single one to collisions where we will see in an example two types of collisions compared: those provided by the VDP itself and box collisions made by software, over which we will have greater control.
Click here for seeing the example working.
Commentaires