Trouble with drawing objects in ScalaFX

61 views Asked by At

I am currently working on a snake game with ScalaFX andd this is my first time working with ScalaFX. Currently, I am having troubles with drawing the snake and the food inside a canvas where the snake and food are not proportional.

I designed the scene with Scenebuilder where I have a vbox with a button where it calls the method to draw the game related items and start the game loop. Below the vbox, there is a canvas which is used for drawing the game and a vbox next to it that displays the player name, score, high score, and a button to exit. This all runs within the same window.

Whenevern I run the game, the food block and the snake are not proportional where it would be extremely large and it looks like it is drawing at the bottom right of the canvas. I had tested this game directly on a file and it works as intended. But when I started to implement the game into scenes using fxml, it just does what I had said.

I had included the screenshots and code related below:

Screenshot of the disporprotion snake and food

First part of the scene

Second part of the scene

Game related methods

package ch.makery.address

import ch.makery.address.model.Position
import ch.makery.address.model.State
import ch.makery.address.view.{AskPlayerNameOverviewController, RulesOverviewController, showGameOverviewController}
import scalafx.application.JFXApp.PrimaryStage
import scalafx.application.{JFXApp, Platform}
import scalafx.beans.property.{IntegerProperty, ObjectProperty}
import scalafx.scene.Scene
import scalafx.scene.paint.Color
import scalafx.scene.paint.Color._
import scalafxml.core.{FXMLLoader, NoDependencyResolver}
import javafx.{scene => jfxs}
import scalafx.Includes._
import scalafx.animation.AnimationTimer
import scalafx.scene.canvas.{Canvas, GraphicsContext}
import scalafx.stage.{Modality, Stage}


import scala.util.Random

object MainApp extends JFXApp{
  val canvasWidth = 600.0
  val canvasHeight = 600.0
  val blockSize = 25

  val rootResource = getClass.getResource("view/MainMenu.fxml")
  val loader = new FXMLLoader(rootResource, NoDependencyResolver)
  loader.load()
  val roots = loader.getRoot[jfxs.layout.AnchorPane]

  stage = new PrimaryStage {
    title = "Greedy Snake"
    scene = new Scene() {
      root = roots
    }
  }

  val initialSnake: List[Position] = List(
    Position(250, 200), // snake head
    Position(225, 200),
    Position(200, 200)
  )

  def showGameOverview(t: String): Unit =
  {
    val direction = IntegerProperty(4) //move to right by default

    val resource = getClass.getResource("view/Game.fxml")
    val loader = new FXMLLoader(resource, NoDependencyResolver)
    loader.load()
    val roots1 = loader.getRoot[jfxs.Parent]
    val control = loader.getController[showGameOverviewController#Controller]

    val dialog = new Stage() {
      title = "GreedySnake.exe"
      initModality(Modality.ApplicationModal)
      initOwner(stage)

      scene = new Scene() {
        root = roots1
        onKeyPressed = key => key.getText match {
          case "w" | "i" => direction.value = 1
          case "a" | "j" => direction.value = 3
          case "s" | "k" => direction.value = 2
          case "d" | "l" => direction.value = 4
          case _ => println("no such inputs")
        }
      }
    }
    control.dialogStage = dialog
    control.playerName = t
    dialog.showAndWait()
  }

  def startGameLoop(canvas: Canvas): Unit = {
    var headColor = Color.Green // Set the initial color of the snake head
    val state = ObjectProperty(State(initialSnake, randomFood(), 0, 0, headColor))
    val frame = IntegerProperty(0)
    val direction = IntegerProperty(4) //move to right by default

    val gc = canvas.graphicsContext2D

    frame.onChange {
      state.update(state.value.newState(direction.value))
    }

    val gameLoop = AnimationTimer(t => {
      var lastUpdateTime: Long = 0
      if (lastUpdateTime == 0)
        lastUpdateTime = 0

      if (t - lastUpdateTime >= 1000000000 / 10) {
        val newState = state.value.newState(direction.value)
        headColor = if (frame.value % 2 == 0) Color.Green else Color.LawnGreen
        state.update(newState.copy(headColor = headColor))
        drawGame(gc, newState)
        lastUpdateTime = t
      }
    })
    gameLoop.start()
  }

  def randomFood(): Position = {
    Position(Random.nextInt(24) * 25, Random.nextInt(24) * 25)
  }

  def drawGame(gc: GraphicsContext, state: State): Unit = {
    gc.clearRect(0, 0, canvasWidth, canvasHeight)
    drawBackground(gc)
    drawBlock(gc, state.food.x, state.food.y, Red)
    println("food x" + state.food.x + " food y" + state.food.y)
    state.snake.foreach{ case Position(x, y) =>
      drawBlock(gc, x, y, if(state.snake.head == Position(x, y)) state.headColor else Green)
      println("snake x" + x + " snake y" + y)
    }
  }

  def drawBackground(gc: GraphicsContext) = {
    for (x <- 0 until canvasWidth.toInt by blockSize) {
      for (y <- 0 until canvasHeight.toInt by blockSize) {
        val color   = if ((x/blockSize + y/blockSize)%2 == 0) LightGreen else LightGoldrenrodYellow
        drawBlock(gc, x, y, color)
      }
    }
  }

  def drawBlock(gc: GraphicsContext, x: Double, y: Double, color: Color): Unit = {
    gc.setFill(color)
    gc.fillRect(x, y, canvasWidth, canvasHeight)
  }
}

package ch.makery.address.model

import ch.makery.address.MainApp.{initialSnake, randomFood}
import scalafx.scene.paint.Color
import scalafx.scene.paint.Color.{LawnGreen, Red}
import scalafx.scene.shape.Rectangle

case class State(snake: List[Position], food: Position, score: Int, highestScore: Int, headColor: Color) {
  def newState(dir: Int): State = {
    val head = snake.head
    val newHead = dir match {
      case 1 => Position(head.x, head.y - 25)
      case 2 => Position(head.x, head.y + 25)
      case 3 => Position(head.x - 25, head.y)
      case 4 => Position(head.x + 25, head.y)
      case _ => head
    }

    if (newHead.x < 0 || newHead.x >= 600 || newHead.y < 0 || newHead.y >= 600 || snake.tail.contains(newHead)) {
      val newSnake = initialSnake
      State(newSnake, food, 0, highestScore, headColor)
    } else if (food == newHead) {
      val newScore = score + 1
      val newHighestScore = if (newScore > highestScore) newScore else highestScore
      State(newHead :: snake, randomFood(), newScore, newHighestScore, headColor)
    } else {
      State(newHead :: snake.init, food, score, highestScore, headColor)
    }
  }

  def rectangles: List[Rectangle] = {
    val (head, body) = snake.splitAt(1)
    square(food.x, food.y, Red) :: square(head.head.x, head.head.y, headColor) :: body.map {
      case Position(x, y) => square(x, y, LawnGreen)
    }
  }

  def square(xr: Double, yr: Double, color: Color) = new Rectangle {
    x = xr
    y = yr
    width = 25
    height = 25
    fill = color
  }
}

package ch.makery.address.model

case class Position(x: Double, y: Double)

Controller

package ch.makery.address.view

import ch.makery.address.MainApp
import scalafxml.core.macros.sfxml
import scalafx.Includes._
import scalafx.event.ActionEvent
import scalafx.scene.canvas.Canvas
import scalafx.scene.control.{Button, Label}
import scalafx.scene.layout.{HBox, VBox}
import scalafx.stage.Stage

@sfxml
class showGameOverviewController(
  private val buttonVBox: VBox,
  private val startGame: Button,
  private val gameHBox: HBox,
  private val gameCanvas: Canvas,
  private val gameVBox: VBox,
  private val nameLabel: Label,
  private val scoreLabel: Label,
  private val highscoreLabel: Label,
  private val exitToMenu: Button) {

  var dialogStage: Stage = null
  var playerName: String = null
  var currentScore: Int = 0
  var highestScore: Int = 0

  def handleShowGameOverview(event: ActionEvent): Unit = {
    buttonVBox.setVisible(false)
    gameVBox.setVisible(true)
    gameHBox.setVisible(true)
    MainApp.startGameLoop(gameCanvas)
  }

  def handleReturnToMenuOverview(event: ActionEvent): Unit = {
    dialogStage.close()
  }
}

FXML

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.canvas.Canvas?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.makery.address.view.showGameOverviewController">
   <children>
      <VBox fx:id="buttonVBox" alignment="CENTER" prefHeight="600.0" prefWidth="800.0" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="5.0" AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0">
         <children>
            <Button fx:id="startGame" mnemonicParsing="false" onAction="#handleShowGameOverview" text="Start Game" />
         </children>
      </VBox>
      <VBox fx:id="gameVBox" alignment="CENTER" layoutX="5.0" layoutY="200.0" prefHeight="590.0" prefWidth="790.0" spacing="125.0" visible="false" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="605.0" AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0">
         <children>
            <Label fx:id="$nameLabel" text="Name" />
            <Label fx:id="scoreLabel" text="Score: $currentScore" />
            <Label fx:id="highscoreLabel" text="Highscore" />
            <Button fx:id="exitToMenu" mnemonicParsing="false" onAction="#handleReturnToMenuOverview" text="Exit to Menu" />
         </children>
      </VBox>
      <HBox fx:id="gameHBox" layoutX="5.0" layoutY="5.0" prefHeight="600.0" prefWidth="600.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="200.0" AnchorPane.topAnchor="0.0">
         <children>
            <Canvas fx:id="gameCanvas" height="600.0" width="600.0" />
         </children>
      </HBox>
   </children>
</AnchorPane>

At first I thought that the whole game was not proportion so I messed with the scaling of the drawings and the coordinates which ended up being worse as it just went out of bounds. Then I added some debugging prints for the food coordinate and the snake coordinate which helped me to discover that the game is most likely working fine and just the drawing is having some problems.

I am also thinking of redoing my scene and have a split pane with the canvas on the left and an anchor pane on the right with the buttons and labels.

0

There are 0 answers