top of page

Pong (la paleta de la CPU)


Esta es la cuarta y última entrega que hemos diseñado para que te programes desde cero una versión sencilla del juego Pong mediante la librería FUSION-C en su versión V1.2.


Al finalizar el artículo quedarán cosas en el tintero, como una mejor gestión de las colisiones o hacer que se pueda seleccionar entre jugar contra un oponente humano.


Si tienes ganas de practicar, te proponemos como un reto programar un segundo jugador utilizando “Q” y “A” para mover la nueva paleta introducida. En cuanto al tema de las colisiones, tenemos en cartera sacar un artículo centrado solo en este tema donde veremos las colisiones por cajas.


¡Estad atentos a las nuevas publicaciones!


Objectivo

 

En el artículo anterior avanzamos hacia lo que sería el artículo final de la serie de cuatro artículos destinada al juego Pong. Hicimos una versión del juego con una sola paleta, utilizando la pared para que la pelota rebotara y devolverla con nuestra paleta. Hicimos lo que sería un juego de frontón.


Ahora ya toca introducir el último elemento que nos falta del juego, la paleta gestionada por la CPU. Esta la vemos marcada con una flecha en la siguiente figura y, como vemos, la situaremos en el extremo opuesto de la paleta del jugador.


Captura de la pantalla inicial del juego

Introducción


Del artículo anterior ya tenemos el sprite de la paleta del jugador. Aprovecharemos el sprite para crear la paleta de la CPU, que posicionaremos en las mismas coordenadas que la del jugador pero en el lado opuesto de la pantalla.


Haremos que esta nueva paleta la mueva el MSX, es decir, el ordenador. Podríamos utilizar un montón de técnicas, pero la más sencilla y eficiente, y la que aplicaremos, será que la paleta siga en todo momento la coordenada Y de la pelota. De esta manera, el ordenador no debería perder nunca, devolviéndonos siempre la pelota hacia nuestro terreno de juego. Esto no será así debido al problema con la gestión de las colisiones que ya comentamos en el artículo anterior.


Sección de documentación


//
// pong12.c
// Fusion-C Pong game example articles
// Here we will finish the articles series by adding the cpu controlled pad
// 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 GameStart(void);
void DrawSprite(void);
void BallCal(void);
void ComputerCal(void);
void AxisCal(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, 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};

Destacar aquí, a diferencia de los otros tres artículos, la definición y utilización de tres sprites en pantalla: la pelota, la paleta controlada por el ordenador y la paleta controlada por el jugador.


Sprite TheBall;
Sprite CpuPad;
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=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);
}

Añadimos a la inicialización de los dos sprites del artículo anterior el tercer sprite.


	// Cpu Pad Sprite initialization
	CpuPad.x=240;
	CpuPad.y=100;
	CpuPad.spr=2;
	CpuPad.pat=1;

En este último artículo ya hacemos uso de las dos variables encargadas de llevar el contador de veces que, por una parte, la CPU no ha sido capaz de devolver la pelota y, por otra, las veces que no ha sido capaz el jugador, a través de las siguientes variables:


	PlyScore=0;
	CpuScore=0;

Como podemos observar, el bucle del juego consta, igual que en el artículo anterior, de dos funciones principales: DrawSprite() y FT_Wait().


En DrawSprite() pintaremos los sprites y controlaremos sus colisiones. También llevaremos el control para que estos se mantengan en todo momento dentro del terreno de juego o pantalla.


Añadimos las funciones BallCal() y ComputerCal(), encargadas de llevar a cabo los cálculos de los movimientos, por una parte, del sprite de la pelota y, por otra, del sprite de la paleta de la CPU.


También os dejamos una función, a modo de depuración, para poder ver en tiempo real las coordenadas de la pelota, AxisCal(). Os la dejamos comentada, pero si queréis podéis descomentarla y compilar vosotros mismos el ejemplo para ver en todo momento las coordenadas (X, Y) de la pelota.



Captura de la pantalla de juego con las coordenadas de la pelota


En FT_Wait() llevaremos un control de retardo en el pintado de pantalla para que con un Z80 todo fluya a una velocidad controlada. Se puede hacer con EnableInterrupt() y Halt(), o con IsVsync() controlando los estados Vblank, pero al tener problemas con las colisiones gestionadas por el propio VDP, hemos optado por hacer 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=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);
}

Con esta función inicializamos la pantalla de juego al principio y cada vez que la supera el terreno de juego del jugador o de la CPU. También se aprovecha para actualizar el marcador con las veces que el jugador o la CPU han superado el terreno de juego del adversario.


Estamos inicializando la dirección de la pelota, los sprites de la pelota, la paleta del jugador y de la CPU, entre otros elementos del juego.


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

Ya comentamos en el artículo anterior que dentro de esta función gestionaremos las colisiones de la pelota con la paleta del jugador. Ahora añadiremos las colisiones con la paleta de la CPU. Como aún no hemos introducido técnicas de colisiones como la de colisiones por cajas, no sabemos al producirse una colisión con qué sprite se ha producido. Esto se debe a que la función SpriteCollision() de la librería vdp_sprites.h de FUSION-C solo nos devuelve un 1 cuando detecta colisión entre sprites.


Hemos ideado una manera sencilla para saber con qué sprite ha colisionado la pelota. Esto ha sido añadiendo dos controles: la coordenada X de la pelota en el momento de la colisión y la dirección de movimiento de la pelota, como podemos ver en el siguiente fragmento de código.


if (SpriteCollision()) {
if(TheBall.x>235 && DirX>0) {
		DirX*=-1;
	}
			
	if (TheBall.x<30 && DirX<0) {
		DirX*=-1;
	} 					
} 

Si os fijáis, también veremos que hemos añadido un control ahora que la pelota puede salir también por los dos terrenos de juego, el del jugador y el de la CPU, para controlar los marcadores y el reinicio de la escena de juego, como podéis ver en el siguiente fragmento de código:


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

En esta función controlaremos las coordenadas de la posición de la pelota, así como que rebote en la parte superior e inferior del terreno de juego.


// Simple Algorythm ! Cpu Cannot be beaten !

void ComputerCal(void)
{
		CpuPad.y=TheBall.y;				
}

Esta función será la encargada de llevar las coordenadas, en concreto la coordenada Y, de la paleta de la CPU. En todo momento igualará la Y de la pelota con la paleta de la CPU. En un escenario de colisiones por cajas, en estas circunstancias, la CPU sería imposible de vencer.


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

Utilizaremos esta función, si descomentáis y compiláis el código vosotros mismos, para ver en pantalla las coordenadas (X,Y) de la pelota en todo momento.


// Wait Routine

void FT_Wait(int cicles) {
  unsigned int i;

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

Por último, la función que os introdujimos en el artículo anterior como una variante de las dos primeras, ya que el enfoque original tenía problemas con las colisiones gestionadas por el VDP.


A continuación os dejamos el código completo y al final un enlace donde podréis ejecutar online el ejemplo en el que hemos trabajado.


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

Conclusiones


¡Hemos llegado al final de la serie de artículos dedicados a la realización de una versión del juego Pong con la librería FUSION-C en su versión V1.2!


Esperamos que te haya servido para introducirte en la programación en C de un juego para la plataforma MSX y que practiques con el resultado al que hemos llegado haciendo tus propias modificaciones. A continuación te dejamos algunas ideas:


  • Puedes cambiar los sprites.

  • Puedes agregar color.

  • Puedes programar un oponente controlado por otro jugador.

  • Puedes incorporar un registro de puntuaciones.

  • Puedes incorporar efectos de sonido o música.

  • Puedes dotar de inteligencia artificial al jugador controlado por la CPU para hacerlo más humano.

 


Clica aquí para ver el ejemplo funcionando.




Comments


bottom of page