top of page

Front Pong

Esta es la tercera de las cuatro entregas que hemos diseñado para que programes desde cero una versión sencilla del juego Pong mediante FUSION-C V1.2.


Objectivo


En el artículo pasado trabajamos en la paleta del jugador y en el primero de la serie la pelota de juego. En este, ubicaremos los dos elementos anteriores dentro del escenario de juego para crear un juego de frontón previo al artículo final, que será el Pong completo.



Ejemplo de la pantalla inicial en la que nos encontraremos al ejecutar fpong.com


Introducción


Al ser el tercer artículo de la serie, ya no presentaremos las secciones en que se divide un archivo de C y pasaremos al grano.


Como hemos dicho, el juego que presentamos aquí es del tipo frontón. La pelota rebotará por las paredes de la zona de juego, como ya lo hacía en el primer artículo. Sin embargo, aquí eliminaremos el rebote de la pared donde está situada la paleta del jugador, permitiendo devolver la pelota golpeándola con esta.


Si el jugador no toca la pelota y esta sobrepasa la paleta del jugador, la CPU incrementará en un punto su marcador.


Por lo tanto, el objetivo es tocar con la paleta la pelota y devolverla tantas veces como sea posible.



Figura explicativa de los rebotes de la pelota que ha hecho contacto con la paleta.


Sección de documentación


//
// fpong.c
// Fusion-C Pong game example articles
// Here we will learn how put the pad and the ball in a game
// MoltSXalats 2024
//

Sección del preprocesador


#include "fusion-c/header/msx_fusion.h"
#include "fusion-c/header/vdp_sprites.h"
#include "fusion-c/header/vdp_graph2.h"
#include <stdlib.h>

Sección de definición de tipos.


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ón de definición de los prototipos de las funciones


void DrawSprite(void);
void BallCal(void);
void GameStart(void);
void FT_Wait(int);

Sección de definición de las variables globales


// 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}; 

Es importante destacar aquí que, a diferencia de los otros dos artículos, definiremos y utilizaremos dos sprites en pantalla: la pelota y la paleta del jugador.


Sprite TheBall;
Sprite PlyPad;

Sección de la función main y el bucle de juego


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);
}

Destacar aquí la inicialización de los dos sprites que tenemos ahora en pantalla.


// 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;

Por primera vez, en este tercer artículo, haremos uso de la variable que llevará la cuenta de la puntuación de la CPU. En este caso, contará las veces que el jugador no sea capaz de devolver la pelota.


// IF MSX is Turbo-R Switch CPU to Z80 Mode
if(ReadMSXtype()==3) {      
ChangeCPU(0);    
}

Si te fijas, hemos agregado un control para que si el programa se ejecuta en un MSX Turbo R, se cambie la CPU a Z80, ya que con la R800 no sería jugable.


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);
}

Como podemos observar, el bucle de juego consta también, al igual que en el artículo pasado, de dos funciones principales: DrawSprite() y FT_Wait().


En DrawSprite() pintaremos los sprites y controlaremos su colisión. También nos aseguraremos de que estos se mantengan en todo momento dentro del área de juego o pantalla.


En FT_Wait() llevaremos a cabo un control de retardo en la actualización de pantalla para que todo fluya a una velocidad controlada en un Z80. Esto se puede hacer con EnableInterrupt() y Halt() o con IsVsync() controlando los estados Vblank, pero debido a problemas con las colisiones gestionadas por el propio VDP, hemos optado por implementar un simple contador de pasos.


Implementación de las funciones definidas por el usuario


// 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);
}

Con esta función inicializamos la pantalla de juego al principio y cada vez que la pelota supere la pala del jugador. También se aprovecha para actualizar el marcador con las veces que la CPU nos ha superado con la pelota.


// 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;
	}
}

La novedad dentro de esta función, que se encarga del pintado de los sprites, es el control de la colisión entre estos, en el caso que nos interesa, de la colisión entre la pelota y la paleta del jugador. Esto se logra utilizando la función SpriteCollision() de la biblioteca vdp_sprites.h de FUSION-C. Esta función devuelve 1 en caso de que se detecte una colisión entre sprites. Aprovechamos el momento de la detección para cambiar la dirección de la pelota DirX*=-1;


En este ejemplo solo trabajaremos con las colisiones que nos proporciona el propio VDP. Facilita mucho el trabajo pero tiene algunos problemas: si vuelve a colisionar antes de que se haya ejecutado el cambio de dirección nos podemos encontrar en un punto muerto, si tuviéramos muchos sprites en pantalla tampoco tendríamos información del sprite que hubiera colisionado.


Por todo lo expuesto, al finalizar el último artículo de la serie dedicaremos un artículo solo a las colisiones por cajas.


// 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;
}

En esta función controlaremos las coordenadas de la posición de la pelota, así como que no salga del área de juego rebotando en tres de las cuatro paredes.


// Wait Routine

void FT_Wait(int cicles) {
  unsigned int i;

  for(i=0;i<cicles;i++)
    {}
}

Por último, la función FT_Wait(), que hemos mencionado anteriormente y que hemos modificado respecto a los otros dos artículos, ya que el enfoque original presentaba problemas con las colisiones gestionadas por el VDP.


A continuación les dejamos el código completo y al final un enlace donde podrán ejecutar online el ejemplo que hemos trabajado.


//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++)
    {}
}

Conclusiones


Ya nos falta poco para tener la serie completa. En el próximo artículo añadiremos la paleta de la CPU controlada por esta y así completaremos la versión del Pong en la que hemos estado trabajando con FUSION-C.


Como hemos comentado en este artículo, dedicaremos uno solo a las colisiones donde veremos en un ejemplo dos de los tipos de colisiones comparadas. Aquellas que proporciona el propio VDP y las de caja realizadas por software, sobre las cuales tendremos un mayor control.


Clica aquí para ver el ejemplo funcionando.



Comments


bottom of page