I'm building a Pac-Man clone for a school project, and I need some help with the ghost movement logic.
In my game, I have a state system:
state = 3 is the initial state, where the ghost exits the ghost room.
Once it leaves, it transitions to state = 0, where it's supposed to move randomly.
The problem is that when the ghost changes from state 3 to state 0, it either: doesn't move at all, moves just a little,or even worse — it goes through the walls.
I'm not sure if it's a logic issue, or if my direction handling is off. Here’s the code I'm using (most variable names and comments are in Italian because it’s my first language).
public class FantasmaRosso extends JPanel implements ActionListener { //Fantasma rosso, tende a rimanere sempre sulla coda di Pac-Man. Segue direttamente Pac-Man
private int blinkyX; // Posizione sull'asse X
private int blinkyY; // Posizione sull'asse Y
private int celleBlinkyX;
private int celleBlinkyY;
private final int velocita; // Velocità del fantasma
private int stato; // Stato del fantasma
private int mangiato; //Chi ha eventualmente mangiato chi
private int direzioneFantasma;
private boolean rifallo;
private BufferedImage immagineBlinkyVersoAlto; //La classe BufferedImage la usiamo per caricare e gestire lo sprite del fantasma nel suo oggetto immagine Blinky
private BufferedImage immagineBlinkyVersoBasso;
private BufferedImage immagineBlinkyVersoDestra;
private BufferedImage immagineBlinkyVersoSinistra;
private Timer timer;
private int pacManX; //X del Pacman
private int pacManY; //Y del Pacman
private int cellePacManX;
private int cellePacManY;
private int vitePacman = 3;
private int precedentiVitePacman;
private final int[][] mappaMatrice;
private final int dimensioneCella;
private double distanzaCelle;
public FantasmaRosso(int dimensioneCella, int pacManX, int pacManY, int[][] mappaMatrice, int vitePacman) {
//VALORI DELLA FINESTRA
this.mappaMatrice = mappaMatrice;
this.dimensioneCella = dimensioneCella;
//VALORI DEL PACMAN
this.pacManX = pacManX;
this.pacManY = pacManY;
//VALORI DEL FANTASMA
this.blinkyX = 9*dimensioneCella;
this.blinkyY = 8*dimensioneCella;
this.velocita = 5;
this.stato = 3;
setBounds(blinkyX, blinkyY, dimensioneCella, dimensioneCella); //posizioniamo e dimensioniamo il componente (= il fantasma) all'interno del contenitore (= mappaMatrice)
this.caricaImmagine();
timer = new Timer(16, this); //Crea un timer che aggiorna ogni 20 millisecondi la posizione dello sprite del fantasma invocando il metodo actionPerformed() definito in questa classe. this richiama il metodo actionPerformed() overridato in questa classe: il metodo actionPerformed appartiene all'interfaccia ActionListener, che va quindi implementata in questa classe
timer.start(); //avviamo il timer
repaint(); //ridisegna il fantasma
}
@Override
public void actionPerformed(ActionEvent e){
celleBlinkyX = blinkyX/dimensioneCella;
celleBlinkyY = blinkyY/dimensioneCella;
cellePacManX = pacManX/dimensioneCella;
cellePacManY = pacManY/dimensioneCella;
if(stato == 3){
if((blinkyX >= 10*dimensioneCella )&& (blinkyY <= 5*dimensioneCella)){
stato = 0;
}
}
/*
if(stato == 0){
distanzaCelle = Math.sqrt( Math.pow(celleX - cellePacManX, 2) + Math.pow(celleY - cellePacManY, 2) ); //Facciamo il teorema di pitagora in modo da considerare se il fantasma dista 5 celle dal pacman verticalmente, orizzontalmente e diagonalmente
if(distanzaCelle <= 5 ){ //se il fantasma dista 5 celle dal pacman (orizzontalmente, verticalmente o diagonalmente)
stato = 1; //passo allo stato di caccia
}
}
*/
this.movimentoFantasma();
/*
if((x == 9*dimensioneCella) && (y == 8*dimensioneCella)){
stato = 3;
}
if(chiHaMangiatoChi() == 1){
stato = 4;
}
*/
repaint();
}
public void caricaImmagine(){
try {
immagineBlinkyVersoAlto = ImageIO.read(new File("Resources/ghost/blinky.png"));//Utilizziamo il metodo read() della classe ImageIO per ottenere e caricare l'immagine del fantasma nell'attributo immagineBlinky. Il pathname usato è relativo.
immagineBlinkyVersoBasso = ImageIO.read(new File("Resources/ghost/blinky.png"));
immagineBlinkyVersoDestra = ImageIO.read(new File("Resources/ghost/blinky.png"));
immagineBlinkyVersoSinistra = ImageIO.read(new File("Resources/ghost/blinky.png"));
} catch (IOException e) { //In caso si verifica un errore (come l'inesistenza del file o un problema di lettura), l'eccezione IOException generata viene catturata da questo catch.
e.printStackTrace(); //Stampa la cronologia dei passaggi fatti per arrivare a questa eccezione.
}
}
public void movimentoFantasma() {
switch(stato) {
//Modalità Casuale: Blinky si muove a caso nella mappa
case 0 -> {
direzioneFantasma = (int)(Math.random() * 4);
switch (direzioneFantasma) {
case 0 -> {
if(mappaMatrice[blinkyY / dimensioneCella][(blinkyX /dimensioneCella) + 1] == 0 || mappaMatrice[blinkyY / dimensioneCella][(blinkyX /dimensioneCella) + 1] == 3){
this.muoviVersoDestra(dimensioneCella);
}
}
case 1 -> {
if(mappaMatrice[blinkyY / dimensioneCella][(blinkyX /dimensioneCella) - 1] == 0 || mappaMatrice[blinkyY / dimensioneCella][(blinkyX /dimensioneCella) - 1] == 3){
this.muoviVersoSinistra(dimensioneCella);
}
}
case 2 -> {
if(mappaMatrice[(blinkyY / dimensioneCella) - 1][blinkyX /dimensioneCella] == 0 || mappaMatrice[(blinkyY / dimensioneCella) - 1][blinkyX /dimensioneCella] == 3){
this.muoviVersoAlto(dimensioneCella);
}
}
case 3 -> {
if(mappaMatrice[(blinkyY / dimensioneCella) + 1][blinkyX /dimensioneCella] == 0 || mappaMatrice[(blinkyY / dimensioneCella) + 1][blinkyX /dimensioneCella] == 3){
this.muoviVersoBasso(dimensioneCella);
}
}
}
}
//Modalità Caccia (Chase): Normale comportamento di Blinky, il quale segue e attacca Pac-Man
case 1 -> {
if ((pacManX > blinkyX && mappaMatrice[blinkyY / dimensioneCella][(blinkyX / dimensioneCella) + 1] == 0)) { //Se il pacman è a destra del fantasma e la cella alla destra del fantasma è vuota (ha valore zero)
direzioneFantasma = 0;
this.muoviVersoDestra(dimensioneCella);
}
else if (pacManX < blinkyX && (mappaMatrice[blinkyY / dimensioneCella][(blinkyX / dimensioneCella) - 1] == 0)) { //Se il pacman è a sinistra del fantasma e la cella alla sinistra del fantasma è vuota (ha valore zero) //per confrontare la posizione del fantasma nella matrice devi dividere la x e la y del fantasma per 50 perché ogni cella della matrice equivale a 50*50 pixel. dopo di chè aggiungi o sottrai 1 alla x o y per muovere il fantasma rosso
direzioneFantasma = 1;
this.muoviVersoSinistra(dimensioneCella);
}
else if (pacManY < blinkyY && (mappaMatrice[(blinkyY / dimensioneCella) - 1][blinkyX / dimensioneCella] == 0)) { //Se il pacman è più alto del fantasma e la cella sopra il fantasma è vuota (ha valore zero)
direzioneFantasma = 2;
this.muoviVersoAlto(dimensioneCella);
}
else if (pacManY > blinkyY && (mappaMatrice[(blinkyY / dimensioneCella) + 1][blinkyX / dimensioneCella] == 0)) { //Se il pacman è più alto del fantasma e la cella sotto il fantasma è vuota (ha valore zero)
direzioneFantasma = 3;
this.muoviVersoBasso(dimensioneCella);
}
this.chiHaMangiatoChi();
}
//Modalità Iniziale: Il fantasma deve uscire dalla stanza dei fantasmi
case 3 -> {
if(blinkyX < 10*dimensioneCella){
direzioneFantasma = 0;
this.muoviVersoDestra(dimensioneCella);
}else if(blinkyY > 5*dimensioneCella){
direzioneFantasma = 1;
this.muoviVersoAlto(dimensioneCella);
}
}
//Modalità Ritorna nella stanza dei fantasmi
case 4 -> {
direzioneFantasma = 0;
blinkyX = 9*dimensioneCella;
blinkyY = 8*dimensioneCella;
}
}
setBounds(blinkyX, blinkyY, dimensioneCella, dimensioneCella); //Aggiorno la posizione dell'immagine del fantasma alla sua posizione attuale4
}
public void muoviVersoDestra(int dimensioneCella){
blinkyX += velocita; //Muovi a destra
}
public void muoviVersoSinistra(int dimensioneCella){
blinkyX -= velocita; // Muovi a sinistra
}
public void muoviVersoAlto(int dimensioneCella){
blinkyY -= velocita; //Muovi verso l'alto
}
public void muoviVersoBasso(int dimensioneCella){
blinkyY += velocita; //Muovi verso il basso
}
//Metodo usato quando Blinky mangia il fantasma rosso
public int chiHaMangiatoChi(){
mangiato = 0;
if((blinkyX /dimensioneCella == pacManX/dimensioneCella) || (blinkyY /dimensioneCella == pacManY/dimensioneCella)){
mangiato = 1;
}
return mangiato;
}
// Metodo per disegnare il fantasma rosso
@Override
public void paintComponent(Graphics g) { //Usiamo l'oggetto g per impostare l'immagine come sfondo del jPanel MappaGioco
super.paintComponent(g); //stiamo chiamando il metodo paintComponents della superclasse JComponent, la quale è la superclasse di JPanel
if(direzioneFantasma == 0){
g.drawImage(immagineBlinkyVersoDestra, 0, 0, this); //Il metodo drawImage() disegna lo sprite di blinky.
// L'oggetto 'this' (che rappresenta questo JPanel) viene utilizzato come ImageObserver.
// Questo è possibile perché JPanel implementa implicitamente l'interfaccia ImageObserver.
// In questo modo, il JPanel stesso è in grado di ricevere notifiche sullo stato di caricamento
// dell'immagine e di ridisegnarsi quando l'immagine è pronta per essere visualizzata.
} else if(direzioneFantasma == 1){
g.drawImage(immagineBlinkyVersoSinistra, 0, 0, this);
} else if(direzioneFantasma == 2){
g.drawImage(immagineBlinkyVersoAlto, 0, 0, this);
} else if(direzioneFantasma == 3){
g.drawImage(immagineBlinkyVersoBasso, 0, 0, this);
}
}
// Metodi get per ottenere le posizioni del fantasma
public int getX() {
return blinkyX;
}
public int getY() {
return blinkyY;
}
//Metodi get e set per ottenere e modificare lo stato del fantasma
public int getStato(){
return stato;
}
public void setStato(int stato) {
this.stato = stato;
}
//Metodi get e set per vedere chi ha mangiato chi
public int getMangiato(){
return mangiato;
}
public void setMangiato(int mangiato){
this.mangiato = mangiato;
}
}
This is the stracture of the matrix I'm using in my code:
mappaMatrice = new int[][]{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1},
{1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 2, 2, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
{1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1},
{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1},
{1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1},
{1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1},
{1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
{1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};
I’d appreciate any tips on how to properly handle: ghost movement only on valid paths (not walls), and switching behavior only when it’s at an intersection or corner.
sorry for the italian parts of the code and for the extensive code.
Thank you to anyone who can answer me.