This is the fourth and final installment that we have designed for you to program a simple version of the Pong game from scratch using the FUSION-C library in its version V1.2.
Upon finishing the article, there will be some unfinished business, such as better collision management or making it possible to select playing against a human opponent.
If you're eager to practice, we propose a challenge: program a second player using "Q" and "A" to move the new paddle introduced. Regarding collision handling, we have plans to release an article focused solely on this topic, where we'll explore box collisions.
Stay tuned for new publications!
Objective
In the previous article, we progressed towards what would be the final article in the series of four articles dedicated to the Pong game. We created a version of the game with a single paddle, using the wall for the ball to bounce off and return it with our paddle. We essentially created what would be a game of frontón.
Now it's time to introduce the final missing element of the game: the CPU-managed paddle. This is marked with an arrow in the following figure, and as we see, it will be placed at the opposite end of the player's paddle.
Screenshot of the game's initial screen.
Introduction
From the previous article, we already have the sprite of the player's paddle. We will use this sprite to create the CPU paddle, which we will position at the same coordinates as the player's paddle but on the opposite side of the screen.
We will make this new paddle move by the MSX, or the computer. We could use a variety of techniques, but the simplest and most efficient one we will apply is to have the paddle always follow the Y coordinate of the ball. This way, the computer should never lose, always returning the ball towards our playing field. However, this won't be the case due to the problem with collision management that we already discussed in the previous article.
Documentation section
//
// pong12.c
// Fusion-C Pong game example articles
// Here we will finish the articles series by adding the cpu controlled pad
// 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;
Secció de definició dels prototipus de les funcions
void GameStart(void);
void DrawSprite(void);
void BallCal(void);
void ComputerCal(void);
void AxisCal(void);
void FT_Wait(int);
Definition section of function prototypes
// Direction of the ball
signed char DirX;
signed char DirY;
char PlyScore,CpuScore;
// Definining the Ball, CPU Pad and Player Pad Structures
Sprite TheBall;
Sprite CpuPad;
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, unlike the other three articles, the definition and use of three sprites on the screen: the ball, the paddle controlled by the computer, and the one controlled by the player.
Sprite TheBall;
Sprite CpuPad;
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=15;
PlyPad.y=100;
PlyPad.spr=1;
PlyPad.pat=1;
PlyScore=0;
// Cpu Pad Sprite initialization
CpuPad.x=240;
CpuPad.y=100;
CpuPad.spr=2;
CpuPad.pat=1;
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();
ComputerCal();
DrawSprite();
//AxisCal();
FT_Wait(200);
}
// Ending program, and return to DOS
Screen(0);
KeySound(1);
Exit(0);
}
We add to the initialization of the two sprites from the previous article the third sprite.
// Cpu Pad Sprite initialization
CpuPad.x=240;
CpuPad.y=100;
CpuPad.spr=2;
CpuPad.pat=1;
In this final article, we already make use of the two variables responsible for keeping track of the number of times, on one hand, the CPU has failed to return the ball, and on the other hand, the times the player has failed, through the following variables:
PlyScore=0;
CpuScore=0;
As we can see, the game loop 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 manage them to ensure that they stay within the playing field or screen at all times.
We add the functions BallCal() and ComputerCal(), responsible for calculating the movements, on one hand, of the ball sprite and, on the other hand, of the CPU paddle sprite.
We also include a function, for debugging purposes, to see the coordinates of the ball in real-time, AxisCal(). We leave it commented out, but if you want, you can uncomment it and compile it yourself, for example, to see the coordinates (X,Y) of the ball at all times.
Screenshot of the game screen with the coordinates of the ball.
In FT_Wait(), we will bring a control of delay in the screen painting so that with a Z80 everything flows at a controlled speed. This can be done with EnableInterrupt() and Halt(), or with IsVsync() controlling the Vblank states, but having problems with collisions managed by the VDP itself, we have opted to make 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=15;
PlyPad.y=100;
CpuPad.x=240;
CpuPad.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 every time the player's or CPU's playing field is surpassed. It is also used to update the score with the number of times the player or the CPU has surpassed the opponent's playing field.
We are initializing the direction of the ball, the ball sprites, the player's paddle, and the CPU's paddle, among other elements of the game.
// Put all sprite on screen
void DrawSprite(void)
{
// Collision Detection
// Collision with CPU Pad or Player Pad Sptite
// Test it in FUSION-C 1.3
/*if (SpriteCollision() && TheBall.x>235 && DirX>0)
DirX*=-1;
if (SpriteCollision() && TheBall.x<15 && DirX<0)
DirX*=-1;*/
// Working in FUSION-C 1.2
if (SpriteCollision()) {
if(TheBall.x>235 && DirX>0) {
DirX*=-1;
}
if (TheBall.x<30 && DirX<0) {
DirX*=-1;
}
}
PutSprite(PlyPad.spr,PlyPad.pat,PlyPad.x,PlyPad.y,15);
PutSprite(CpuPad.spr,CpuPad.pat,CpuPad.x,CpuPad.y,15);
PutSprite(TheBall.spr,TheBall.pat,TheBall.x,TheBall.y,15);
// Check Ball Outside Game field
if (TheBall.x<15)
{
CpuScore++;
GameStart();
}
if (TheBall.x>245)
{
PlyScore++;
GameStart();
}
// Check PlyPad Outside Game field
if (PlyPad.y<10)
{
PlyPad.y=10;
}
if (PlyPad.y>190)
{
PlyPad.y=190;
}
}
We already mentioned in the previous article that within this function we will handle collisions between the ball and the player's paddle. Now we will add collisions with the CPU's paddle. Since we haven't introduced collision techniques like box collisions yet, we don't know which sprite has collided when a collision occurs. This is because the SpriteCollision() function from the vdp_sprites.h library in FUSION-C only returns a 1 when it detects a collision between sprites.
We have devised a simple way to determine which sprite the ball has collided with. We have added two controls: the X coordinate of the ball at the time of the collision and the direction of movement of the ball, as we can see in the following code fragment.
if (SpriteCollision()) {
if(TheBall.x>235 && DirX>0) {
DirX*=-1;
}
if (TheBall.x<30 && DirX<0) {
DirX*=-1;
}
}
If you notice, we have also added a control so that the ball can now exit from both the player's and the CPU's playing fields, in order to manage the score and reset the game scene, as you can see in the following code fragment:
if (TheBall.x<15)
{
CpuScore++;
GameStart();
}
if (TheBall.x>245)
{
PlyScore++;
GameStart();
}
// 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;
}
In this function, we will control the coordinates of the ball's position, as well as ensure that it bounces off the top and bottom of the playing field.
// Simple Algorythm ! Cpu Cannot be beaten !
void ComputerCal(void)
{
CpuPad.y=TheBall.y;
}
This function will be responsible for managing the coordinates, specifically the Y coordinate, of the CPU paddle. It will always match the Y coordinate of the ball with the CPU paddle. In a scenario with box collisions, under these circumstances, the CPU would be impossible to defeat.
// Prints Ball Coordinates
void AxisCal(void)
{
char Temp[5];
Itoa(TheBall.x,Temp,10);
if(StrLen(Temp)==1)
{
if(TheBall.x==9)
PutText(100,4," ",0);
PutText(100,4,Temp,0);
}
if(StrLen(Temp)==2) {
if(TheBall.x==99)
PutText(100,4," ",0);
PutText(100,4,Temp,0);
}
if(StrLen(Temp)==3)
{
PutText(100,4,Temp,0);
}
Itoa(TheBall.y,Temp,10);
if(StrLen(Temp)==1)
{
if(TheBall.y==9)
PutText(140,4," ",0);
PutText(140,4,Temp,0);
}
if(StrLen(Temp)==2) {
if(TheBall.y==99)
PutText(140,4," ",0);
PutText(140,4,Temp,0);
}
if(StrLen(Temp)==3)
{
PutText(140,4,Temp,0);
}
}
We will use this function if you uncomment and compile the code yourself to see the coordinates (X,Y) of the ball on the screen at all times.
// Wait Routine
void FT_Wait(int cicles) {
unsigned int i;
for(i=0;i<cicles;i++)
{}
}
Finally, the function that we introduced to you in the previous article as a variant of the first two, since the original approach was causing problems with collisions managed by the VDP.
Below, we provide you with the entire code, and at the end, a link where you can execute the example we've been working on online.
//Documentation section
//
// pong12.c
// Fusion-C Pong game example articles
// Here we will finish the articles series by adding the cpu controlled pad
// 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 GameStart(void);
void DrawSprite(void);
void BallCal(void);
void ComputerCal(void);
void AxisCal(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, CPU Pad and Player Pad Structures
Sprite TheBall;
Sprite CpuPad;
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=15;
PlyPad.y=100;
PlyPad.spr=1;
PlyPad.pat=1;
PlyScore=0;
// Cpu Pad Sprite initialization
CpuPad.x=240;
CpuPad.y=100;
CpuPad.spr=2;
CpuPad.pat=1;
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();
ComputerCal();
DrawSprite();
//AxisCal();
FT_Wait(200);
}
// Ending program, and return to DOS
Screen(0);
KeySound(1);
Exit(0);
}
// 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=15;
PlyPad.y=100;
CpuPad.x=240;
CpuPad.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
// Collision with CPU Pad or Player Pad Sptite
// Test it in FUSION-C 1.3
/*if (SpriteCollision() && TheBall.x>235 && DirX>0)
DirX*=-1;
if (SpriteCollision() && TheBall.x<15 && DirX<0)
DirX*=-1;*/
// Working in FUSION-C 1.2
if (SpriteCollision()) {
if(TheBall.x>235 && DirX>0) {
DirX*=-1;
}
if (TheBall.x<30 && DirX<0) {
DirX*=-1;
}
}
PutSprite(PlyPad.spr,PlyPad.pat,PlyPad.x,PlyPad.y,15);
PutSprite(CpuPad.spr,CpuPad.pat,CpuPad.x,CpuPad.y,15);
PutSprite(TheBall.spr,TheBall.pat,TheBall.x,TheBall.y,15);
// Check Ball Outside Game field
if (TheBall.x<15)
{
CpuScore++;
GameStart();
}
if (TheBall.x>245)
{
PlyScore++;
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;
}
// Simple Algorythm ! Cpu Cannot be beaten !
void ComputerCal(void)
{
CpuPad.y=TheBall.y;
}
// Prints Ball Coordinates
void AxisCal(void)
{
char Temp[5];
Itoa(TheBall.x,Temp,10);
if(StrLen(Temp)==1)
{
if(TheBall.x==9)
PutText(100,4," ",0);
PutText(100,4,Temp,0);
}
if(StrLen(Temp)==2) {
if(TheBall.x==99)
PutText(100,4," ",0);
PutText(100,4,Temp,0);
}
if(StrLen(Temp)==3)
{
PutText(100,4,Temp,0);
}
Itoa(TheBall.y,Temp,10);
if(StrLen(Temp)==1)
{
if(TheBall.y==9)
PutText(140,4," ",0);
PutText(140,4,Temp,0);
}
if(StrLen(Temp)==2) {
if(TheBall.y==99)
PutText(140,4," ",0);
PutText(140,4,Temp,0);
}
if(StrLen(Temp)==3)
{
PutText(140,4,Temp,0);
}
}
// Wait Routine
void FT_Wait(int cicles) {
unsigned int i;
for(i=0;i<cicles;i++)
{}
}
Conclusions
We have reached the end of the series of articles dedicated to creating a version of the Pong game using the FUSION-C library in its version V1.2.
We hope it has served as an introduction to C programming for MSX platform games and that you practice with the result we have achieved by making your own modifications. Here are some ideas:
You can change the sprites.
You can add color.
You can program an opponent controlled by another player.
You can incorporate scoreboards.
You can incorporate sound effects or music.
You can give AI to the player controlled by the CPU to make it more human-like.
Click here for seeing the example working.
Comments