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