top of page

Pong (la pala de la CPU)


Aquesta és la quarta i darrera entrega que hem dissenyat perquè et programis des de zero una versió senzilla del joc Pong mitjançant la llibreria FUSION-C en la seva versió V1.2. 

 

En finalitzar l’article quedaran coses al tinter, com una millor gestió de les col·lisions o fer que es pugui seleccionar entre jugar contra un oponent humà.

 

Si tens ganes de practicar, et proposem com un repte programar un segon jugador utilitzant “Q” i “A” per moure la nova pala introduïda. Pel que fa al tema de les col·lisions tenim en cartera treure un article centrat sols en aquest tema on veurem les col·lisions per caixes.

 

Estigueu atents a les noves publicacions!


Objectiu

 

Al passat article vam avançar cara al que seria l’article final de la sèrie de quatre articles destinada al joc Pong. Van fer una versió del joc amb una sola pala, fent servir la paret per tal que la pilota hi rebotés i tornar-la amb la nostra pala. Vam fer el que seria un joc de frontó. 

 

Ara ja toca introduir el darrer element que ens falta del joc, la pala gestionada per la CPU. Aquesta la veiem marcada amb una fletxa a la següent figura i com veiem la situarem a l’extrem oposat de la pala del jugador.


Captura de la pantalla inicial de joc

Introducció


Del passat article ja tenim el Sprite de la pala del jugador. Aprofitarem el Sprite per a crear la pala de la CPU que posicionarem a les mateixes coordenades que el del jugador però al costat oposat de la pantalla.

 

Farem que aquesta nova pala la mogui el MSX, o sigui l’ordinador. Podríem utilitzar un grapat de tècniques, però la més senzilla i eficient i la que aplicarem serà que la pala segueixi en tot moment la coordenada Y de la pilota. D’aquesta manera l’ordinador no hauria de perdre mai tornant-nos sempre la pilota cap al nostre terreny de joc. Això no serà així pel problema amb la gestió de les col·lisions que ja vam comentar al passat article.


Secció de documentació


//
// pong12.c
// Fusion-C Pong game example articles
// Here we will finish the articles series by adding the cpu controlled pad
// MoltSXalats 2024
//

Secció del preprocessador


#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ó de definició de tipus


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

Secció de definició de les variables globals


// 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 diferència dels altres tres articles, la definició i utilització de tres Sprites en pantalla: la pilota, la pala controlada per l’ordinador i la controlada pel jugador.


Sprite TheBall;
Sprite CpuPad;
Sprite PlyPad;

Secció de la funció main i el bucle de joc


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

Afegim a la inicialització dels dos Sprites de l’anterior article el tercer Sprite.


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

Aquest darrer article ja fem ús de les dues variables encarregades de portar  el comptador de vegades que per una banda la CPU no ha estat capaç de tornar la pilota i per una altra les que no ha estat capaç el jugador a través de les següents variables:


	PlyScore=0;
	CpuScore=0;

Com podem observar el bucle de joc consta també igual que en el passat article de dues funcions principals: DrawSprite() i FT_Wait().


A DrawSprite() pintarem els sprites així com en controlarem la seva col·lisió. També portarem el control per tal que aquests es mantinguin en tot moment dins el terreny de joc o pantalla.


Afegim les funcions BallCal() i ComputerCal() encarregades del portar els càlculs dels moviuments per una banda del Sprite de la pilota i per l’altre del Sprite de la paleta de la CPU.


També us deixem una funció, a mode de debug, per poder veure en temps real les coordenades de la pilota AxisCal(). Us la deixem comentada, però si voleu la podeu des comentar i compilar vosaltres mateixos, l’exemple per veure en tot moment les coordenades (X,Y) de la pilota.



Captura de la pantalla de joc amb les coordenades de la pilota


A FT_Wait() portarem un control de retard en el pintat de pantalla per tal que amb un Z80 tot flueixi a una velocitat controlada. Es pot fer amb EnableInterrupt() i Halt() o amb IsVsync() controlant els estats Vblank, però en tenir problemes amb les col·lisions gestionades pel propi VDP, hem optat per fer un simple comptador de passos.


Implementació de les funcions definides per l’usuari


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

Amb aquesta funció inicialitzem la pantalla de joc al principi i cada vegada que la supera el terreny de joc del jugador o de la CPU. S'aprofita també per actualitzar el marcador amb les vegades que el jugador o la CPU ha superat el terreny de joc de l’adversari.


Estem inicialitzant la direcció de la pilota, els Sprites pilota, pala del jugador i de la CPU entre altres elements del joc.


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

Ja vam comentar a l'anterior article que dins aquesta funció gestionarem les col·lisions de la pilota amb la pala del jugador. Ara hi afegirem les col·lisions amb la pala de la CPU. Al no haver introduït encara les tècniques de col·lisions com la de col·lisions per caixes no sabem al produir-se una col·lisió amb quin Sprite s’ha produït. Això ja que la funció SpriteCollision()de la llibreria vdp_sprites.h de FUSION-C sols ens retorna un 1 quan detecta col·lisió entre Sprites.


Hem ideat una manera senzilla per saber com quin Sprite ha col·lisionat la pilota. Aquesta ha estat afegit dos controls la coordenada X de la pilota en el moment de la col·lisió i la direcció de moviment de la pilota com podem veure al següent fragment de codi.


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

Si us fixeu també veurem que hem afegit un control ara que la pilota pot sortir també pels dos terrenys de joc, del jugador i del la CPU, per controlar els marcador i el reinici de l’escena de joc com podeu veure al següent fragment de codi:


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 aquesta funció controlarem les coordenades de la posició de la pilota, així com que reboti a la part superior i inferior del terreny de joc.


// Simple Algorythm ! Cpu Cannot be beaten !

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

Aquesta funció serà l’encarregada de portar les coordenades, en concret la coordenada Y, de la pala de la CPU. En tot moment igualarà la Y de la pilota amb la pala de la CPU. En un escenari en col·lisions per caixes, en aquestes circumstàncies,  la CPU seria impossible de vèncer.


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

Farem servir aquesta funció, si descomenteu i compileu el codi vosaltres mateixos, per veure en pantalla les coordenades (X,Y) de la pilota en tot moment.


// Wait Routine

void FT_Wait(int cicles) {
  unsigned int i;

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

Per últim la funció que us vam introduir al passat article com una variant del dos primers, ja que l’enfoc original portava problemes amb les col·lisions gestionades pel VDP.



Tot seguit us deixem el codi sencer i al final un enllaç on podreu executar online l’exemple que  hem treballat.


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


Ja hem arribat al final de la sèrie d’articles dedicades a la realització d’una versió del joc Pong amb la llibreria FUSION-C en la seva versió V1.2.

 

Esperem que t’hagi servit per introduir-te a la programació en C d’un joc per a la plataforma MSX i que practiquis amb el resultat al que hem arribat fent-hi les teves modificacions, tot seguit et deixem algunes idees:

 

  • Pots canviar els Sprites

  • Pots afegir-hi color

  • Pots programar un oponent portat per una altre jugador

  • Pots incorporar un registres de puntuacions

  • Pots incorporar efectes de so o música

  • Pots dotar d'IA al jugador portat per la CPU per a fer-lo més humà

 


Clica aquí per a veure l'exemple funcionant.




Commentaires


bottom of page