Search code examples
javaandroidcallbacksurfaceviewappcompatactivity

How can a View react to a Method call of a SurfaceView?


RESUME
I'm programming a pacman, it is almost done but I'm having problems with the communication between the view which will show the score and the currect direction of the pacman, and the surface view which will draw the game.

The PlayActivity (which is an AppCompatActivity) has a GameView(which is a SurfaceView where the game is draw) and knows the GameManager(a class I've made that basically knows the pacman, the map, the ghosts, and everything). The PlayActivity also has a text view called scoreTv. I don't know how to update this text view.

The idea is that the scoreTv change its text value whenever the GameManager's methods addScore(int),eatPallet(int, int), eatBonus(int,int); eatSuperPallet(int,int) are invoked.

Here is the PlayActivity code

package Activities;

import android.os.Bundle;
import android.view.SurfaceView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.example.pacman.DBManager;
import Game.GameView;
import com.example.pacman.R;
import com.example.pacman.Score;

public class PlayActivity extends AppCompatActivity {
    private TextView playerNickname;
    TextView score;
    private TextView maxScore;
    private SurfaceView gameSurfaceView;
    private GameView gameView;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //Modified code
        setContentView(R.layout.activity_game);
        //we get text view that we will use
        playerNickname=(TextView) this.findViewById(R.id.tv_player);
        score=(TextView) this.findViewById(R.id.tv_current_score);
        maxScore=(TextView) this.findViewById(R.id.tv_current_max_score);
        gameSurfaceView= (GameView) this.findViewById(R.id.game_view);

        //set text view initial values
        playerNickname.setText(getIntent().getExtras().getString("playerNickname"));
        score.setText("0");

        maxScore.setText("To modify");


        this.gameView=new GameView(gameSurfaceView.getContext());
        this.gameSurfaceView.getHolder().addCallback(this.gameView);
    }

    protected void onResume(){
        super.onResume();
        this.gameView.resume();
    }
    protected void onPause(){
        super.onPause();
        this.gameView.pause();
    }

    public void updateScore(int score){
        this.score.setText(""+score);
    }

    public void onLose(double score){
        //We try to save the score, if there is a previous register we write only if this score
        //is better that the one before
        DBManager manager;
        long raw;
        Score scoreToSave;
        manager=new DBManager(this);

        scoreToSave=new Score(this.playerNickname.toString(), score);
        if(manager.saveScore(scoreToSave)==-1){
            //if i couldn't save the score
            if(manager.updateScore(scoreToSave)!=-1){
                //if my new score is better than the one previous
            }else{
                //if my new score is worse or equal than the one previous
            }
        }
    }
}

And here is the Game View code

package Game;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.TextView;

import androidx.annotation.RequiresApi;

import Activities.PlayActivity;
import Game.Character_package.Ghost;
import Game.Character_package.Pacman;

public class GameView extends SurfaceView implements Runnable, SurfaceHolder.Callback, GestureDetector.OnGestureListener {
    private static final float SWIPE_THRESHOLD = 2;
    private static final float SWIPE_VELOCITY = 2;
    private boolean GHOST_INICIALIZED=false;
    private GestureDetector gestureDetector;
    private GameManager gameManager;
    private Thread thread; //game thread
    private SurfaceHolder holder;
    private boolean canDraw = false;
    private int blockSize;                // Ancho de la pantalla, ancho del bloque
    private static int movementFluencyLevel=8; //this movement should be a multiple of the blocksize and multiple of 4, if note the pacman will pass walls

    private int totalFrame = 4;             // Cantidad total de animation frames por direccion
    private int currentArrowFrame = 0;      // animation frame de arrow actual
    private long frameTicker;               // tiempo desde que el ultimo frame fue dibujado

    //----------------------------------------------------------------------------------------------
    //Constructors
    public GameView(Context context) {
        super(context);
        this.constructorHelper(context);
    }

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.constructorHelper(context);
    }

    public GameView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.constructorHelper(context);

    }

    private void constructorHelper(Context context) {
        this.gestureDetector = new GestureDetector(this);
        this.setFocusable(true);
        this.holder = getHolder();
        this.holder.addCallback(this);
        this.frameTicker = (long) (1000.0f / totalFrame);

        this.gameManager=new GameManager(this);

        int screenWidth=getResources().getDisplayMetrics().widthPixels;
        this.blockSize = ((((screenWidth/this.gameManager.getGameMap().getMapWidth())/movementFluencyLevel)*movementFluencyLevel)/4)*4;
        this.holder.setFixedSize(blockSize*this.gameManager.getGameMap().getMapWidth(),blockSize*this.gameManager.getGameMap().getMapHeight());

        this.gameManager.getGameMap().loadBonusBitmaps(this);
        this.gameManager.setPacman(new Pacman("pacman","",this,this.movementFluencyLevel));

        Ghost.loadCommonBitmaps(this);
    }
    //----------------------------------------------------------------------------------------------
    //Getters and setters
    public int getBlockSize() {
        return blockSize;
    }
    public GameManager getGameManager() {
        return gameManager;
    }
    public int getMovementFluencyLevel(){return movementFluencyLevel;}
    //----------------------------------------------------------------------------------------------

    private synchronized void initGhost(){
        if(!GHOST_INICIALIZED){
            GHOST_INICIALIZED=true;
            this.gameManager.initGhosts(this);
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    public void run() {
        long gameTime;
        Canvas canvas;
        while (!holder.getSurface().isValid()) {
        }
        this.initGhost();
        this.setFocusable(true);
        while (canDraw) {
            gameTime=System.currentTimeMillis();
            if(gameTime > frameTicker + (totalFrame * 15)){
                canvas = holder.lockCanvas();
                if(canvas!=null){
                    if(this.updateFrame(gameTime,canvas)){
                        try {
                            Thread.sleep(3000);
                        }catch (Exception e){}
                    }
                    holder.unlockCanvasAndPost(canvas);
                    if(this.gameManager.checkWinLevel()){
                        canDraw=false;
                        this.gameManager.cancelThreads();
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {}
                        //animation
                        Log.i("Game","You win");
                    }else if(!this.gameManager.getPacman().hasLifes()){
                        //we lost

                        canDraw=false;
                        this.gameManager.cancelThreads();

                        //animation
                        Log.i("Game","You lose");
                    }
                }
            }
        }

    }

    // Method to capture touchEvents
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //To swipe
        //https://www.youtube.com/watch?v=32rSs4tE-mc
        this.gestureDetector.onTouchEvent(event);
        super.onTouchEvent(event);
        return true;
    }

    //Chequea si se deberia actualizar el frame actual basado en el
    // tiempo que a transcurrido asi la animacion
    //no se ve muy rapida y mala
    @RequiresApi(api = Build.VERSION_CODES.N)
    private boolean updateFrame(long gameTime, Canvas canvas) {
        Pacman pacman;
        Ghost[] ghosts;
        boolean pacmanIsDeath;

        pacman=this.gameManager.getPacman();
        ghosts=this.gameManager.getGhosts();

        // Si el tiempo suficiente a transcurrido, pasar al siguiente frame
        frameTicker = gameTime;
        canvas.drawColor(Color.BLACK);
        this.gameManager.getGameMap().draw(canvas, Color.BLUE,this.blockSize,this.gameManager.getLevel());
        this.gameManager.moveGhosts(canvas,this.blockSize);
        pacmanIsDeath=pacman.move(this.gameManager,canvas);

        if(!pacmanIsDeath){
            // incrementar el frame
            pacman.changeFrame();
            for(int i=0; i<ghosts.length;i++){
                ghosts[i].changeFrame();
            }
            currentArrowFrame++;
            currentArrowFrame%=7;
        }else{
            pacman.setNextDirection(' ');
            for(int i=0; i<ghosts.length;i++){
                ghosts[i].respawn();
            }
        }
        return pacmanIsDeath;
    }

    //----------------------------------------------------------------------------------------------
    //Callback methods
    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        canDraw = true;
        this.thread= new Thread(this);
        this.thread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

    //----------------------------------------------------------------------------------------------
    public void resume() {
        this.canDraw = true;
        thread = new Thread(this);
        thread.start();
    }

    public void pause() {
        this.canDraw = false;
        while (true) {
            try {
                thread.join();
                return;
            } catch (InterruptedException e) {
                // retry
            }
            break;
        }
        this.thread=null;
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onFling(MotionEvent downEvent, MotionEvent moveEvent, float velocityX, float velocityY) {
        //To swipe
        //https://www.youtube.com/watch?v=32rSs4tE-mc
        float diffX, diffY;
        Pacman pacman;
        Log.i("Fling", "detected");

        diffX = moveEvent.getX() - downEvent.getX();
        diffY = moveEvent.getY() - downEvent.getY();
        pacman=this.gameManager.getPacman();

        if (Math.abs(diffX) > Math.abs(diffY)) {
            //right or left swipe
            if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY) {
                if (diffX > 0) {
                    //right
                    pacman.setNextDirection('r');
                } else {
                    //left
                    pacman.setNextDirection('l');
                }
            }

        } else {
            //up or down swipe
            if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY) {
                if (diffY > 0) {
                    //down
                    pacman.setNextDirection('d');
                } else {
                    //up
                    pacman.setNextDirection('u');
                }
            }
        }
        return true;
    }

}

And finally the GameManager code

package Game;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.util.Log;
import android.widget.TextView;

import androidx.annotation.RequiresApi;

import com.example.pacman.R;

import org.jetbrains.annotations.NotNull;

import Game.Behavior.ChaseBehavior.*;
import Game.Character_package.Ghost;
import Game.Character_package.Pacman;
import Game.GameCountDown.*;


public class GameManager {
    private static final int TOTAL_LEVELS=256;
    private GameMap gameMap;
    private int level,bonusResetTime,score;
    private CountDownScareGhosts scareCountDown;
    private Pacman pacman;
    private Ghost[] ghosts;
    private boolean fruitHasBeenInTheLevel;

    public GameManager(GameView gameView){
        this.fruitHasBeenInTheLevel=false;
        this.score=0;
        this.gameMap=new GameMap();
        this.gameMap.loadMap1();
        this.level=1;
        this.ghosts=new Ghost[4];
        this.bonusResetTime = 5000;
        this.scareCountDown=null;
    }

    public void addScore(int s){
        this.score+=s;
    }

    public int getScore() {
        return this.score;
    }
    public int getLevel() {
        return this.level;
    }
    public GameMap getGameMap() {
        return this.gameMap;
    }
    public Ghost[] getGhosts(){
        return this.ghosts;
    }
    public Ghost getGhost(int i) {
        return this.ghosts[i];
    }
    public Pacman getPacman(){
        return this.pacman;
    }
    public void setPacman(Pacman pacman){
        this.pacman=pacman;
    }


    public void eatPallet(int posXMap, int posYMap){
        this.score+=10;
        //Log.i("Score", Double.toString(this.score).substring(0,Double.toString(this.score).indexOf('.')));
        this.gameMap.getMap()[posYMap][posXMap]=0;
    }

    public void eatBonus(int posXMap,int posYMap){
        this.score+=500;
        //Log.i("Score", Double.toString(this.score).substring(0,Double.toString(this.score).indexOf('.')));
        this.gameMap.getMap()[posYMap][posXMap]=0;
    }

    public void eatSuperPallet(int posXMap,int posYMap){
        this.score+=50;
        Log.i("Score", Double.toString(this.score).substring(0,Double.toString(this.score).indexOf('.')));
        this.gameMap.getMap()[posYMap][posXMap]=0;

        //Si hay un timer andando lo cancelo y ejecuto otro
        if (this.scareCountDown != null){
            this.scareCountDown.cancel();
        }
        this.scareCountDown = new CountDownScareGhosts(this.ghosts,this.gameMap.getMap());
        this.scareCountDown.start();
    }

    public void tryCreateBonus(){
        //only if pacman has eaten 20 pallets we should allow the fruit appear
        if(!this.fruitHasBeenInTheLevel && this.gameMap.getEatenPallets()>=20){
            //to not allow the fruit be again in the level
            this.fruitHasBeenInTheLevel=true;
            new CountdownBonusThread(this.gameMap,this.bonusResetTime).start();
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    public void moveGhosts(Canvas canvas,int blocksize) {
        for (int i = 0; i < ghosts.length; i++) {
            ghosts[i].move(this.gameMap.getMap(),this.pacman);
            ghosts[i].draw(canvas);
        }
    }

    public synchronized void initGhosts(@NotNull GameView gv) {
        int[][]spawnPositions,cornersPositions, notUpDownPositions,defaultTargets;
        int movementeFluency;

        defaultTargets=this.gameMap.getDefaultGhostTarget();
        notUpDownPositions=this.gameMap.getNotUpDownDecisionPositions();
        spawnPositions=this.gameMap.getGhostsSpawnPositions();
        cornersPositions=this.gameMap.getGhostsScatterTarget();
        movementeFluency=gv.getMovementFluencyLevel();
        //start position
        // 5 blinky spawn [13, 11]
        // 6 pinky spawn [15,11]
        // 7 inky spawn [13,16]
        // 8 clyde spawn [15,16]
        this.ghosts=new Ghost[4];
        ghosts[0] = new Ghost("blinky",gv,spawnPositions[0], cornersPositions[0] ,new BehaviorChaseAgressive(notUpDownPositions,movementeFluency,defaultTargets[0]),movementeFluency,notUpDownPositions,'l',defaultTargets[0]);
        ghosts[1] = new Ghost("pinky",gv,spawnPositions[1],cornersPositions[1],new BehaviorChaseAmbush(notUpDownPositions,movementeFluency,defaultTargets[1]),movementeFluency,notUpDownPositions,'r',defaultTargets[1]);
        ghosts[2] = new Ghost("inky",gv,spawnPositions[2],cornersPositions[2],new BehaviorChasePatrol(notUpDownPositions,this.ghosts[0],movementeFluency,defaultTargets[0]),movementeFluency,notUpDownPositions,'l',defaultTargets[0]);
        ghosts[3] = new Ghost("clyde",gv,spawnPositions[3],cornersPositions[3],new BehaviorChaseRandom(notUpDownPositions,cornersPositions[3],movementeFluency,defaultTargets[1]),movementeFluency,notUpDownPositions,'r',defaultTargets[1]);

        try{
            Thread.sleep(200);
        }catch(Exception e){}

        for (int i=0;i<ghosts.length;i++){
            ghosts[i].onLevelStart(1);
        }

    }

    public boolean checkWinLevel() {
        //player win the level if he has eaten all the pallet
        return this.gameMap.countPallets()==0;
    }

    public void onResume(){
        for (int i=0 ; i<this.ghosts.length;i++){
            this.ghosts[i].cancelBehavoirThread();
        }
        if(this.scareCountDown!=null && !this.scareCountDown.hasEnded()){
            this.scareCountDown.start();
        }
    }

    public void onPause(){
        for (int i=0 ; i<this.ghosts.length;i++){
            this.ghosts[i].cancelBehavoirThread();
        }
        if(this.scareCountDown!=null && !this.scareCountDown.hasEnded()){
            this.scareCountDown=this.scareCountDown.onPause();
        }
    }

    public void cancelThreads(){
        for (int i=0 ; i<this.ghosts.length;i++){
            this.ghosts[i].cancelBehavoirThread();
        }
        if(this.scareCountDown!=null){
            this.scareCountDown.cancel();
        }
    }

}

What I know so far is that I can't sent the scoreTv to the GameView, because the GameView can't change it due to not being in the same thread. I need the PlayActivity instance react whenever any of the methods I've told you before are called.


Solution

  • Well, I have found a solution, I don't like it, tough. What have I done is the next:

    1. I have created a STATIC SEMAPHORE in the AppCompatActivity
    2. I pass it to the class GameManager as a STATIC SEMAPHORE too and I've changed the score in this class to static
    3. Finally I've run a thread in the PlayActivity with the method runOnUiThread to update the scoreTv, this thread will acquire a permit, this one will be relese whenever the SCORE is update in the GameManager to avoid the active wait

    You may be asking why I remark STATIC. If I don't do this, I don't know why the reference of the GameManager get lost. If someone know the answer please post it. Here a link to the git repo, and here the code of the classes

    public class PlayActivity extends AppCompatActivity {
        private TextView playerNickname;
        private TextView scoreTv;
        private TextView maxScore;
        private SurfaceView gameSurfaceView;
        private GameView gameView;
        private GameManager gameManager;
        private static Semaphore CHANGE_SCORE_MUTEX=new Semaphore(0,true);
        private static Semaphore CHANGE_DIRECTION_MUTEX=new Semaphore(0,true);
        private Thread changeScoreThread, changeDirectionThread;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            //Modified code
            setContentView(R.layout.activity_game);
            //we get text view that we will use
            playerNickname=(TextView) this.findViewById(R.id.tv_player);
            scoreTv=(TextView) this.findViewById(R.id.tv_current_score);
            maxScore=(TextView) this.findViewById(R.id.tv_current_max_score);
            gameSurfaceView= (GameView) this.findViewById(R.id.game_view);
    
            //set text view initial values
        
      playerNickname.setText(getIntent().getExtras().getString("playerNickname"));
           scoreTv.setText("0");
    
           maxScore.setText("To modify");
    
           this.gameView=new GameView(gameSurfaceView.getContext());
           this.gameManager=this.gameView.getGameManager();
           this.gameView.setSemaphores(CHANGE_SCORE_MUTEX,CHANGE_DIRECTION_MUTEX);
           this.gameSurfaceView.getHolder().addCallback(this.gameView);
        }    
    
        protected void onResume(){
            super.onResume();
            this.gameView.resume();
            this.initChangerThreads();
        }
    
        public void updateScoreTv(int score){
            this.scoreTv.setText(""+score);
        }
    
        protected void onPause(){
            super.onPause();
            this.gameView.pause();
            //in order to stop the threads
            CHANGE_SCORE_MUTEX.release();
            CHANGE_DIRECTION_MUTEX.release();
        }
    
        public void onLose(double score){
            //We try to save the score, if there is a previous register we write only if this score
            //is better that the one before
            DBManager manager;
            long raw;
            Score scoreToSave;
            manager=new DBManager(this);
    
            scoreToSave=new Score(this.playerNickname.toString(), score);
            if(manager.saveScore(scoreToSave)==-1){
                //if i couldn't save the score
                if(manager.updateScore(scoreToSave)!=-1){
                    //if my new score is better than the one previous
                }else{
                    //if my new score is worse or equal than the one previous
                }
            }
        }   
    
        private void initChangerThreads() {
            this.changeScoreThread = new Thread(new Runnable() {
            public void run() {
                while (gameView.isDrawing()) {
                    //Log.i("Score ",""+gameManager.getScore());
                    try {
                        CHANGE_SCORE_MUTEX.acquire();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                updateScoreTv(gameView.getGameManager().getScore());
                            }
                        });
                    }catch (Exception e){}
                 }
            }
            });
            this.changeScoreThread.start();
        }
    }
    

    GameView: I've just added this method

    public void setSemaphores(Semaphore changeScoreSemaphore, Semaphore changeDirectionSemaphore){
        this.gameManager.setChangeScoreSemaphore(changeScoreSemaphore);
        this.gameManager.getPacman().setChangeDirectionSemaphore(changeDirectionSemaphore);
        Log.i("Semaphore", "setted");
    }
    

    GameManager

    public class GameManager {
        private static final int TOTAL_LEVELS=256;
        private static int SCORE=0;
        private GameMap gameMap;
        private int level,bonusResetTime;//,score;
        private CountDownScareGhosts scareCountDown;
        private Pacman pacman;
        private Ghost[] ghosts;
        private boolean fruitHasBeenInTheLevel;
        private static Semaphore CHANGE_SCORE_MUTEX;
    
        public GameManager(){
            this.fruitHasBeenInTheLevel=false;
            //this.score=0;
            this.gameMap=new GameMap();
            this.gameMap.loadMap1();
            this.level=1;
            this.ghosts=new Ghost[4];
            this.bonusResetTime = 5000;
            this.scareCountDown=null;
        }
    
        public void setChangeScoreSemaphore(Semaphore changeScoreSemaphore) {
            CHANGE_SCORE_MUTEX = changeScoreSemaphore;
            //if(this.changeScoreSemaphore==null){
            //    Log.i("Change Score Semaphore","I'm null");
            //}else{
            //    Log.i("Change Score Semaphore","I'm not null");
            //}
        }
    
        public void addScore(int s){
            //this.score+=s;
            SCORE+=s;
            CHANGE_SCORE_MUTEX.release();
            /*if(this.changeScoreSemaphore==null){
                Log.i("Change Score Semaphore","I'm null");
            }else{
                Log.i("Change Score Semaphore","I'm not null");
            }*/
            //this.changeScoreSemaphore.release();
        }
    
        public int getScore() {
            return SCORE;
            //return this.score;
        }
    
        public int getLevel() {
            return this.level;
        }
        public GameMap getGameMap() {
            return this.gameMap;
        }
        public Ghost[] getGhosts(){
            return this.ghosts;
        }
        public Pacman getPacman(){
            return this.pacman;
        }
        public void setPacman(Pacman pacman){
            this.pacman=pacman;
        }
    
    
        public void eatPallet(int posXMap, int posYMap){
            SCORE+=10;
            CHANGE_SCORE_MUTEX.release();
            //this.score+=10;
            Log.i("Score GM", ""+SCORE);
            //Log.i("Score GM", ""+this.score);
            this.gameMap.getMap()[posYMap][posXMap]=0;
            //this.changeScoreSemaphore.release();
            //if(this.changeScoreSemaphore==null){
            //    Log.i("Change Score Semaphore","I'm null");
            //}else{
            //    Log.i("Change Score Semaphore","I'm not null");
            //}
        }
    
        public void eatBonus(int posXMap,int posYMap){
            SCORE+=500;
            CHANGE_SCORE_MUTEX.release();
            //this.score+=500;
            //Log.i("Score", Double.toString(this.score).substring(0,Double.toString(this.score).indexOf('.')));
            this.gameMap.getMap()[posYMap][posXMap]=0;
            //this.changeScoreSemaphore.release();
        }
    
        public void eatSuperPallet(int posXMap,int posYMap){
            SCORE+=50;
            CHANGE_SCORE_MUTEX.release();
            //this.score+=50;
            this.gameMap.getMap()[posYMap][posXMap]=0;
    
            //Si hay un timer andando lo cancelo y ejecuto otro
            if (this.scareCountDown != null){
                this.scareCountDown.cancel();
            }
            this.scareCountDown = new CountDownScareGhosts(this.ghosts,this.gameMap.getMap());
            this.scareCountDown.start();
            //this.changeScoreSemaphore.release();
        }
    
        public void tryCreateBonus(){
            //only if pacman has eaten 20 pallets we should allow the fruit appear
            if(!this.fruitHasBeenInTheLevel && this.gameMap.getEatenPallets()>=20){
                //to not allow the fruit be again in the level
                this.fruitHasBeenInTheLevel=true;
                new CountdownBonusThread(this.gameMap,this.bonusResetTime).start();
            }
        }
    
        @RequiresApi(api = Build.VERSION_CODES.N)
        public void moveGhosts(Canvas canvas,int blocksize) {
            for (int i = 0; i < ghosts.length; i++) {
                ghosts[i].move(this.gameMap.getMap(),this.pacman);
                ghosts[i].draw(canvas);
            }
        }
    
        public synchronized void initGhosts(int blocksize, Resources res, String packageName,int movementFluency) {
            int[][]spawnPositions,cornersPositions, notUpDownPositions,defaultTargets;
    
            defaultTargets=this.gameMap.getDefaultGhostTarget();
            notUpDownPositions=this.gameMap.getNotUpDownDecisionPositions();
            spawnPositions=this.gameMap.getGhostsSpawnPositions();
            cornersPositions=this.gameMap.getGhostsScatterTarget();
            //start position
            // 5 blinky spawn [13, 11]
            // 6 pinky spawn [15,11]
            // 7 inky spawn [13,16]
            // 8 clyde spawn [15,16]
            this.ghosts=new Ghost[4];
            ghosts[0] = new Ghost("blinky",spawnPositions[0], cornersPositions[0] ,new BehaviorChaseAgressive(notUpDownPositions,movementFluency,defaultTargets[0]),movementFluency,notUpDownPositions,'l',defaultTargets[0],blocksize,res,packageName);
            ghosts[1] = new Ghost("pinky",spawnPositions[1],cornersPositions[1],new BehaviorChaseAmbush(notUpDownPositions,movementFluency,defaultTargets[1]),movementFluency,notUpDownPositions,'r',defaultTargets[1],blocksize,res,packageName);
            ghosts[2] = new Ghost("inky",spawnPositions[2],cornersPositions[2],new BehaviorChasePatrol(notUpDownPositions,this.ghosts[0],movementFluency,defaultTargets[0]),movementFluency,notUpDownPositions,'l',defaultTargets[0],blocksize,res,packageName);
            ghosts[3] = new Ghost("clyde",spawnPositions[3],cornersPositions[3],new BehaviorChaseRandom(notUpDownPositions,cornersPositions[3],movementFluency,defaultTargets[1]),movementFluency,notUpDownPositions,'r',defaultTargets[1],blocksize,res,packageName);
    
            try{
                Thread.sleep(200);
            }catch(Exception e){}
    
            for (int i=0;i<ghosts.length;i++){
                ghosts[i].onLevelStart(1);
            }
    
        }
    
        public boolean checkWinLevel() {
            //player win the level if he has eaten all the pallet
            return this.gameMap.countPallets()==0;
        }
    
        public void onResume(){
            for (int i=0 ; i<this.ghosts.length;i++){
                this.ghosts[i].cancelBehavoirThread();
            }
            if(this.scareCountDown!=null && !this.scareCountDown.hasEnded()){
                this.scareCountDown.start();
            }
        }
    
        public void onPause(){
            for (int i=0 ; i<this.ghosts.length;i++){
                this.ghosts[i].cancelBehavoirThread();
            }
            if(this.scareCountDown!=null && !this.scareCountDown.hasEnded()){
                this.scareCountDown=this.scareCountDown.onPause();
            }
        }
    
        public void cancelThreads(){
            for (int i=0 ; i<this.ghosts.length;i++){
                this.ghosts[i].cancelBehavoirThread();
            }
            if(this.scareCountDown!=null){
                this.scareCountDown.cancel();
            }
        }
    
    }
    

    I haven't found a solution like this anywhere, I wish I've save you a lot of research time if you're currently having a problem like this :D