MouseListener not getting events

Code is in Scala, and much of it is copy/pasted (with appropriate translation to Scala) from the Zay-Es sample code for Asteroid Panic. I think I got everything that is currently in use - let me know if I missed something.

The issue is that in ToyModelFactory, there is a mouse event listener set up, but I’m not seeing any of those callbacks - as evidenced by a complete lack of the appropriate logging messages.

I’m a complete neophyte, so it’s a dead certainty that I’ve missed an important item somewhere. Any hints or suggestions would be appreciated.

Main.scala:

object Main {
  def main(args: Array[String]): Unit = {
    val main = DemoApp()
    main.start()
  }
}

class DemoApp extends SimpleApplication(
  new EntityDataState(),
  new ModelState(new ToyModelFactory()),
  new MainMenuState()
) {
  private val logger: Logger = LoggerFactory.getLogger(getClass.getName)

  override def simpleInitApp(): Unit = {
    logger.debug("[DemoApp.simpleInitApp] enter.")

    val settings = getContext.getSettings

    // Initialize the Lemur helper instance
    GuiGlobals.initialize(this)

    // Setup the "retro" style for our HUD and GUI elements
    val styles = GuiGlobals.getInstance.getStyles
    ToyStyles.initializeStyles(styles)
  }
}

MatchGameState.scala:

class MatchGameState extends BaseAppState {
  private val logger: Logger = LoggerFactory.getLogger(getClass.getName)
  private var app: DemoApp = _
  private var assets: AssetManager = _
  private var container: Container = _
  private var inputManager: InputManager = _
  private var x: Geometry = _

  private var ed: EntityData = _
  private var entities: EntitySet = _
  private var cardID: EntityId = _

  override def initialize(app: Application): Unit = {
    logger.debug("[MatchGameState.initialize] enter")
    this.app = app.asInstanceOf[DemoApp]
    assets = app.getAssetManager

    ed = getState(classOf[EntityDataState]).getEntityData
    entities = ed.getEntities(classOf[Position], classOf[Position])

    cardID = ed.createEntity
    ed.setComponents(
      cardID,
      new Position(new Vector3f(-2.0, -2.0, 0.0), new Quaternion),
      new ModelType(ToyModelFactory.MATCH_CARD, "Lion")
    )

    logger.debug("[MatchGameState.initialize] cardID is: {}", cardID)
  }

  override def cleanup(app: Application): Unit = {
    logger.debug("[MatchGameState.cleanup] enter.")
  }

  override def onEnable(): Unit = {
    logger.debug("[MatchGameState.onEnable] enter.")
//    app.getGuiNode.attachChild(x)
  }

  override def onDisable(): Unit = {
    logger.debug("[MatchGameState.onDisable] enter.")
  }
}

ToyModelFactory.scala:

object ToyModelFactory {
  val ENTITY_ID = "entityID"

  val MATCH_CARD = "match game card"
}

class ToyModelFactory extends ModelFactory {
  private val logger: Logger = LoggerFactory.getLogger(getClass.getName)

  private var state: ModelState = _
  private var assets: AssetManager = _
  private var ed: EntityData = _

  override def setState(state: ModelState): Unit = {
    this.state = state
    this.assets = state.getApplication.getAssetManager
    this.ed = state.getApplication.getStateManager.getState(classOf[EntityDataState]).getEntityData
  }


  override def createModel(e: Entity): Spatial = {
    val modelType = e.get(classOf[ModelType])
    logger.debug("[ToyModelFactory.createModel] creating: {}", modelType)

    val id = e.getId.getId
    val parent = new Node()
    parent.setUserData(ToyModelFactory.ENTITY_ID, id)

    val label = modelType.getLabel
    val fileName = s"$label.png"
    val t: Texture = assets.loadTexture(fileName)

    val mesh = createMesh()
    val front = new Geometry("Card", mesh)
    front.setUserData(ToyModelFactory.ENTITY_ID, id)
    front.move(-0.55f, -0.8f, 0f)
    val mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md")

    mat.setTexture("ColorMap", t)
    front.setMaterial(mat)

    val back = new Geometry("Card", mesh)
    back.setUserData(ToyModelFactory.ENTITY_ID, id)
    val mat2 = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md")
    mat2.setColor("Color", ColorRGBA.Blue)
    back.setMaterial(mat2)
    back.move(0.55f, -0.8f, 0.01f)
    back.rotate(0f, FastMath.PI, 0f)

    parent.attachChild(front)
    parent.attachChild(back)
    MouseEventControl.addListenersToSpatial(parent, MouseListener)

    parent
  }

  private def createMesh(): Mesh = {
    val mesh = new Mesh()

    val vertices = new Array[Vector3f](4)
    vertices(0) = new Vector3f(0, 0, 0)
    vertices(1) = new Vector3f(1.1, 0, 0)
    vertices(2) = new Vector3f(0, 1.6, 0)
    vertices(3) = new Vector3f(1.1, 1.6, 0)

    val texCoord = new Array[Vector2f](4)
    texCoord(0) = new Vector2f(0, 0)
    texCoord(1) = new Vector2f(1, 0)
    texCoord(2) = new Vector2f(0, 1)
    texCoord(3) = new Vector2f(1, 1)

    val indexes = Array(2, 0, 1, 1, 3, 2)

    mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(vertices:_*))
    mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoord:_*))
    mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indexes:_*))

    mesh
  }

  object MouseListener extends DefaultMouseListener {
    override def click(event: MouseButtonEvent, target: Spatial, capture: Spatial): Unit = {
      val entityID = target.getUserData(ToyModelFactory.ENTITY_ID)
      logger.debug("[MouseListener.click] target: {}", entityID)
    }

    override def mouseButtonEvent(event: MouseButtonEvent, target: Spatial, capture: Spatial): Unit = {
      val entityID = target.getUserData(ToyModelFactory.ENTITY_ID)
      logger.debug("[MouseListener.mouseButtonEvent] target: {}", entityID)
    }

    override def mouseEntered(event: MouseMotionEvent, target: Spatial, capture: Spatial): Unit = {
      val entityID = target.getUserData(ToyModelFactory.ENTITY_ID)
      logger.debug("[MouseListener.mouseEntered] target: {}", entityID)
    }

    override def mouseExited(event: MouseMotionEvent, target: Spatial, capture: Spatial): Unit = {
      val entityID = target.getUserData(ToyModelFactory.ENTITY_ID)
      logger.debug("[MouseListener.mouseExited] target: {}", entityID)
    }

    override def mouseMoved(event: MouseMotionEvent, target: Spatial, capture: Spatial): Unit = {
      val entityID = target.getUserData(ToyModelFactory.ENTITY_ID)
      logger.debug("[MouseListener.mouseMoved] target: {}", entityID)
    }
  }
}

MainMenuState.scala:

class MainMenuState() extends BaseAppState {
  private val logger: Logger = LoggerFactory.getLogger(getClass.getName)
  private var menu: Container = _

  override def initialize(app: Application): Unit = {
    logger.debug("[MainMenuState.initialize] enter.")
    menu = new Container(new SpringGridLayout, new ElementId(ToyStyles.MENU_ID), "retro")

    menu.addChild(new Label("Match Game", new ElementId(ToyStyles.MENU_TITLE_ID), "retro"))

    val start = menu.addChild(new Button("Start Game", "retro"))
    start.addClickCommands(new Start())
    start.addCommands(Button.ButtonAction.HighlightOn, new Highlight())

    val exit = menu.addChild(new Button("Exit", "retro"))
    exit.addClickCommands(new Stop())
    exit.addCommands(Button.ButtonAction.HighlightOn, new Highlight())

    val cam = app.getCamera
    val menuScale = cam.getHeight / 720f

    val pref = menu.getPreferredSize
    menu.setLocalTranslation(cam.getWidth * 0.5f - pref.x * 0.5f * menuScale, cam.getHeight * 0.75f + pref.y * 0.5f * menuScale, 10)
    menu.setLocalScale(menuScale)
  }

  override def cleanup(app: Application): Unit = {
    logger.debug("[MainMenuState.cleanup] enter.")
  }

  override def onEnable(): Unit = {
    logger.debug("[MainMenuState.onEnable] enter.")
    val app = getApplication.asInstanceOf[DemoApp]
    app.getGuiNode.attachChild(menu)
  }

  override def onDisable(): Unit = {
    logger.debug("[MainMenuState.onDisable] enter.")
    menu.removeFromParent()
  }

  private class Highlight extends Command[Button] {
    override def execute(source: Button): Unit = {
      logger.debug("[Highlight.execute] enter.")
    }
  }

  private class Start extends Command[Button] {
    override def execute(source: Button): Unit = {
      logger.debug("[Start.execute] enter.")
      getStateManager.attach(new MatchGameState)
      setEnabled(false)
    }
  }

  private class Stop extends Command[Button] {
    override def execute(source: Button): Unit = {
      logger.debug("[Stop.execute] enter.")
      getApplication.stop()
    }
  }
}

ModelType.scala:

/**
 * A general "model type" used for entities with a visual
 * display.
 *
 * @author Paul Speed
 *         Converted to Scala by IntelliJ IDEA
 */
class ModelType(private var myType: String, private var label: String) extends EntityComponent {
  def getType: String = myType
  def getLabel: String = label

  override def toString: String = s"ModelType[$myType, $label]"
}

EntityDataState.scala:

/**
 * AppState providing convenient access to the global
 * ES entity data.  It also properly cleans up the ES
 * upon termination.
 *
 * @author Paul Speed
 *         Converted to Scala by IntelliJ IDEA
 */
class EntityDataState(private var entityData: EntityData) extends BaseAppState {
  private val logger: Logger = LoggerFactory.getLogger(getClass.getName)

  def this() = {
    this(new DefaultEntityData)
  }

  def getEntityData: EntityData = entityData

  override protected def initialize(app: Application): Unit = {
  }

  override protected def cleanup(app: Application): Unit = {
    entityData.close()
    entityData = null // cannot be reused

  }

  override protected def onEnable(): Unit = {
  }

  override protected def onDisable(): Unit = {
  }
}

ModelFactory.scala:

/**
 * Called by the ModelState to create new Spatials when
 * required.
 *
 * @author Paul Speed
 *         Converted to Scala by IntelliJ IDEA
 */
trait ModelFactory {
  def setState(state: ModelState): Unit

  def createModel(e: Entity): Spatial
}

Position.scala:

/**
 * Represents a position and orientation of an entity.
 *
 * @author Paul Speed
 *         Converted to Scala by IntelliJ IDEA
 */
class Position(private var location: Vector3f, private var facing: Quaternion) extends EntityComponent {
  def getLocation: Vector3f = location

  def getFacing: Quaternion = facing

  override def toString: String = "Position[" + location + ", " + facing + "]"
}

Where is “parent” attached to a scene?

Since it does show up in the display, I thought that was a thing that Zay-Es did? Asteroid Panic doesn’t have any clickable elements, so there must be another step that I’ve missed.

If it shows up on the screen then something must attach it to something that is connected to the root or guiNode. I didn’t know if your app made its own viewport or something which is why I asked.

Try running the Lemur Gem #3 Lemur Gems #3 : Scene picking

…and work from there.

Normally, anything in the scene is clickable unless something is setup weird.

Sure, I’ll take a look at that.

Ok, so I went through the code I posted a bit more carefully - there are parts I lifted from Asteroid Panic directly, not quite understanding them. And, in fact, I completely missed posting one file, the file where the geometry objects are added to the root node:

ModelState.scala:

/**
 * Watches entities with Position and ModelType components
 * and creates/destroys Spatials as needed as well as moving
 * them to the appropriate locations.
 * Spatials are created with a ModelFactory callback object
 * that can be game specific.
 * 
 * Note: currently model type changes are not detected.
 *
 * @author Paul Speed
 *         Converted to Scala by IntelliJ IDEA
 */
//noinspection ScalaWeakerAccess
class ModelState(private var factory: ModelFactory) extends BaseAppState {
  private val logger = LoggerFactory.getLogger(classOf[ModelState].getName)

  private var ed: EntityData = _
  private var entities: EntitySet = _
  private val models = new util.HashMap[EntityId, Spatial]
  private var modelRoot: Node = _

  def getSpatial(entity: EntityId): Spatial = models.get(entity)

  protected def createSpatial(e: Entity): Spatial = factory.createModel(e)

  protected def addModels(set: util.Set[Entity]): Unit = {
    logger.debug("[ModelState.addModels] adding: {}", set)
    val i = set.iterator()
    while (i.hasNext) {
      val nextEntity = i.next()
      // See if we already have one
      var s = models.get(nextEntity.getId)
      if (s != null) {
        logger.error("Model already exists for added entity: {}", nextEntity)
      } else {
        s = createSpatial(nextEntity)
        models.put(nextEntity.getId, s)
        updateModelSpatial(nextEntity, s)
        modelRoot.attachChild(s)
      }
    }
  }

  protected def removeModels(set: util.Set[Entity]): Unit = {
    val i = set.iterator()
    while (i.hasNext) {
      val e = i.next()
      val s = models.remove(e.getId)
      if (s == null) {
        logger.error("Model not found for removed entity: {}", e)
      } else {
        s.removeFromParent()
      }
    }
  }

  protected def updateModelSpatial(e: Entity, s: Spatial): Unit = {
    val p = e.get(classOf[Position])
    s.setLocalTranslation(p.getLocation)
    s.setLocalRotation(p.getFacing)
  }

  protected def updateModels(set: util.Set[Entity]): Unit = {
    val i = set.iterator()
    while (i.hasNext) {
      val e = i.next()
      val s = models.get(e.getId)
      if (s == null) {
        logger.error("Model not found for updated entity:" + e)
      } else {
        updateModelSpatial(e, s)
      }
    }
  }

  override protected def initialize(app: Application): Unit = {
    logger.debug("[ModelState.initialize] enter.")
    factory.setState(this)
    // Grab the set of entities we are interested in
    ed = getState(classOf[EntityDataState]).getEntityData
    entities = ed.getEntities(classOf[Position], classOf[ModelType])
    // Create a root for all of the models we create
    modelRoot = new Node("Model Root")
  }

  override protected def cleanup(app: Application): Unit = {
    // Release the entity set we grabbed previously
    entities.release()
    entities = null
  }

  override protected def onEnable(): Unit = {
    logger.debug("[ModelState.onEnable] enter.")
    getApplication.asInstanceOf[DemoApp].getRootNode.attachChild(modelRoot)
    entities.applyChanges
    addModels(entities)
  }

  override def update(tpf: Float): Unit = {
    if (entities.applyChanges) {
      removeModels(entities.getRemovedEntities)
      addModels(entities.getAddedEntities)
      updateModels(entities.getChangedEntities)
    }
  }

  override protected def onDisable(): Unit = {
    modelRoot.removeFromParent()
    removeModels(entities)
  }
}

If you are not seeing any of the mouse events at all (enter/exit/move/etc.) and are otherwise seeing your other debug logs… then I’m at a loss to explain what is happening.

It may be that you have to load things into a debugger and set some breakpoints in the Lemur mouse handling to see where things get hung up. I think the listener Lemur registers with JME is done in MouseAppState or something like that.

I was able to see it process the mouse click event. It is checking my meshes, but not getting a collision.

It is pretty much exactly as the debug output that I posted to the Discord:

17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] name: pick target, value: 0.013732125, tpf: 0.013732125
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] entity: Entity[EntityId[0], values=[Position[(-2.0, -2.0, 0.0), (0.0, 0.0, 0.0, 1.0)]]]
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] location: (-2.0, -2.0, 0.0)
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] screen location: (204.11774, 124.11775, 0.9009009)
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] click2d: (206.0, 129.0)
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] click3d: (-0.19675142, -0.19157375, 9.0)
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] dir: (-0.18972763, -0.1847348, -0.964301)
17:23:50.729 [jME3 Main] DEBUG org.primitive.MatchGameState - [ClickHandler.onAnalog] result: CollisionResults

Maybe replace your objects with standard JME boxes or spheres or something to see if it starts working.

Maybe you forgot to call:
Mesh.UpdateBound() or Geometry.updateModelBound()
on your custom meshes. Don’t remember which one, or both are required

2 Likes

Good point. updateBound() on the mesh will work before adding it to the Geometry.

If you change the mesh after it’s already a part of a Geometry then you would need to call Geometry.updateModelBound().

1 Like

Adding a call parent.updateModelBound() fixed it.

Thank you!

2 Likes