I've been working on switching my applications from Swing to JavaFX. I've been working on a room escape game which displays a description of the item on which the user clicks. In Swing, I'd subclass JComponent and override the paintComponent(Graphics) method. I could draw the text there, knowing that the method is constantly called to update the screen. However, using the JavaFX Canvas, there is no method that is called constantly, which makes this task harder. I attempted save()ing the GraphicsContext after I drew the images and called restore() when I wanted to remove the text, but to no avail. Here's the important code:
package me.nrubin29.jescape;
import javafx.application.Platform;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.shape.Rectangle;
import java.util.Timer;
import java.util.TimerTask;
public class RoomPane extends Canvas {
private Room room;
private Toast toast;
public RoomPane() {
super(640, 480);
setOnMouseClicked(e -> {
for (JObject o : room.getObjects()) {
if (o.getBounds().contains(e.getX(), e.getY())) {
toast = new Toast(o.getDescription());
}
}
});
new Timer().schedule(new TimerTask() {
#Override
public void run() {
if (toast == null) {
return;
}
if (toast.decrement()) { // Decrements the internal counter. If the count is 0, this method returns true.
toast = null;
Platform.runLater(() -> getGraphicsContext2D().restore());
}
else {
Platform.runLater(() -> getGraphicsContext2D().strokeText(toast.getText(), 300, 100));
}
}
}, 0, 1000);
}
public void changeRoom(Room room) {
this.room = room;
GraphicsContext g = getGraphicsContext2D();
g.drawImage(room.getBackground(), 0, 0);
for (JObject o : room.getObjects()) {
g.drawImage(o.getImage(), getCenterX(o.getBounds()), getCenterY(o.getBounds()));
}
g.save();
}
}
I attempted save()ing the GraphicsContext after I drew the images and called restore() when I wanted to remove the text, but to no avail.
save and restore have nothing to with removing things like text, what they do is save in a stack the state of various settings like a stroke or fill to use to draw shapes and allow them to be popped off the stack for application later. Those routines don't effect the pixels drawn on the canvas at all.
To remove something from a GraphicsContext, you can either draw over the of it, or clear it. For your code, what you could do is snapshot the canvas node where you are trying to save it, then draw your snapshot image onto the canvas where you are trying to restore it. It is probably not the most efficient way of handling drawing (a smarter routine which just draws only damaged area where the text is would be better, but probably not required for your simple game).
However, using the JavaFX Canvas, there is no method that is called constantly
Rather than using a timer to trigger canvas calls, use a AnimationTimer or a Timeline. The AnimationTimer has a callback method which is invoked every pulse (60 times a second, or as fast as JavaFX can render frames, whichever is the lesser), so it gives you an efficient hook into the JavaFX pulse based rendering system. The Timeline can have keyframes which are invoked at user specified durations and each keyframe can have an event handler callback which is invoked at that duration.
Using the built-in JavaFX animation framework, you don't have to worry about multi-threading issues and doing things like Platform.runLater which overly complicate your code and can easily lead to subtle and serious errors.
On a kind of unrelated note, for a simple game like this, IMO you are probably better off recoding it completely to use the JavaFX scene graph rather than a canvas. That way you will be working at a higher level of abstraction rather than clip areas and repainting damaged paint components.
Related
Even the official documentation has borderline insane recommendations to solve what is probably one of the most common UI/3D interaction issues:
If I click while the cursor is over a UI button, both the button (via the graphics raycaster) and the 3D world (via the physics raycaster) will receive the event.
The official manual:
https://docs.unity3d.com/Packages/com.unity.inputsystem#1.2/manual/UISupport.html#handling-ambiguities-for-pointer-type-input essentially says "how about you design your game so you don't need 3D and UI at the same time?".
I cannot believe this is not a solved problem. But everything I've tried failed. EventSystem.current.currentSelectedGameObject is sticky, not hover. PointerData is protected and thus not accessible (and one guy offered a workaround via deriving your own class from Standalone Input Module to get around that, but that workaround apparently doesn't work anymore). The old IsPointerOverGameObject throws a warning if you query it in the callback and is always true if you query it in Update().
That's all just mental. Please someone tell me there's a simple, obvious solution to this common, trivial problem that I'm just missing. The graphics raycaster certainly stores somewhere if it's over a UI element, right? Please?
I've looked into this a fair bit and in the end, the easiest solution seems to be to do what the manual says and put it in the Update function.
bool pointerOverUI = false;
void Update()
{
pointerOverUI = EventSystem.current.IsPointerOverGameObject();
}
Your frustration is well founded: there are NO examples of making UI work with NewInput which I've found. I can share a more robust version of the Raycaster workaround, from Youtube:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.UI;
/* Danndx 2021 (youtube.com/danndx)
From video: youtu.be/7h1cnGggY2M
thanks - delete me! :) */
public class SCR_UiInteraction : MonoBehaviour
{
public GameObject ui_canvas;
GraphicRaycaster ui_raycaster;
PointerEventData click_data;
List<RaycastResult> click_results;
void Start()
{
ui_raycaster = ui_canvas.GetComponent<GraphicRaycaster>();
click_data = new PointerEventData(EventSystem.current);
click_results = new List<RaycastResult>();
}
void Update()
{
// use isPressed if you wish to ray cast every frame:
//if(Mouse.current.leftButton.isPressed)
// use wasReleasedThisFrame if you wish to ray cast just once per click:
if(Mouse.current.leftButton.wasReleasedThisFrame)
{
GetUiElementsClicked();
}
}
void GetUiElementsClicked()
{
/** Get all the UI elements clicked, using the current mouse position and raycasting. **/
click_data.position = Mouse.current.position.ReadValue();
click_results.Clear();
ui_raycaster.Raycast(click_data, click_results);
foreach(RaycastResult result in click_results)
{
GameObject ui_element = result.gameObject;
Debug.Log(ui_element.name);
}
}
}
So, just drop into my "Menusscript.cs"?
But as a pattern, this is terrible for separating UI concerns. I'm currently rewiring EVERY separately-concerned PointerEventData click I had already working, and my question is, Why? I can't even find how it's supposed to work: to your point there IS no official guide at all around clicking UI, and it does NOT just drop-on-top.
Anyway, I haven't found anything yet which makes new input work easily on UI, and definitely not found how I'm going to sensibly separate Menuclicks from Activityclicks while keeping game & ui assemblies separate.
Good luck to us all.
Unity documentation for this issue with regard to Unity.InputSystem can be found at https://docs.unity3d.com/Packages/com.unity.inputsystem#1.3/manual/UISupport.html#handling-ambiguities-for-pointer-type-input.
IsPointerOverGameObject() can always return true if the extent of your canvas covers the camera's entire field of view.
For clarity, here is the solution which I found worked best (accumulated from several other posts across the web).
Attach this script to your UI Canvas object:
public class CanvasHitDetector : MonoBehaviour {
private GraphicRaycaster _graphicRaycaster;
private void Start()
{
// This instance is needed to compare between UI interactions and
// game interactions with the mouse.
_graphicRaycaster = GetComponent<GraphicRaycaster>();
}
public bool IsPointerOverUI()
{
// Obtain the current mouse position.
var mousePosition = Mouse.current.position.ReadValue();
// Create a pointer event data structure with the current mouse position.
var pointerEventData = new PointerEventData(EventSystem.current);
pointerEventData.position = mousePosition;
// Use the GraphicRaycaster instance to determine how many UI items
// the pointer event hits. If this value is greater-than zero, skip
// further processing.
var results = new List<RaycastResult>();
_graphicRaycaster.Raycast(pointerEventData, results);
return results.Count > 0;
}
}
In class containing the method which is handling the mouse clicks, obtain the reference to the Canvas UI either using GameObject.Find() or a public exposed variable, and call IsPointerOverUI() to filter clicks when over UI.
Reply to #Milad Qasemi's answer
From the docs you have attached in your answer, I have tried the following to check if the user clicked on a UI element or not.
// gets called in the Update method
if(Input.GetMouseButton(0) {
int layerMask = 1 << 5;
// raycast in the UI layer
RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero, Mathf.Infinity, layerMask);
// if the ray hit any UI element, return
// don't handle player movement
if (hit.collider) { return; }
Debug.Log("Touched not on UI");
playerController.HandlePlayerMovement(x);
}
The raycast doesn't seem to detect collisions on UI elements. Below is a picture of the Graphics Raycaster component of the Canvas:
Reply to #Lowelltech
Your solution worked for me except that instead of Mouse I used Touchscreen
// Obtain the current touch position.
var pointerPosition = Touchscreen.current.position.ReadValue();
An InputSytem is a way to receive new inputs provided by Unity. You can't use existing scripts there, and you'll run into problems like the original questioner. Answers with code like "if(Input.GetMouseButton(0)" are invalid because they use the old system.
I want to put loader in between dialog boxes come up for the purchase. What is the way for this?
Because when game player press Buy button, he should require to wait for 5 to 10 second depends on internet speed and server response and this process happed 2 to 3 times because multiple dialogs come up within screen.
So in this case, may be player can leave the screen. I want to put the loader so that game player realise that some processing is running in background, he required to wait for some time.
At present I was following completely this code for Unity IAP setup.
Integrating Unity IAP In Your Game
I assume this is for mobile platform but even if its not still the following can be considered:
Simple solution is to create a full screen Image (UI/Panel) object in your UI to block clicks. I would use Animator component (with triggers) to display this panel in front of other UI when there is a background process running.
public class Loader : MonoBehaviour
{
public static Loader Instance;
Animator m_Animator;
public bool Loading {get; private set;}
void Awake()
{
Instance = this; // However make sure there is only one object containing this script in the scene all time.
}
void Start()
{
//This gets the Animator, which should be attached to the GameObject you are intending to animate.
m_Animator = gameObject.GetComponent<Animator>();
Loading = false;
}
public void Show()
{
Loading = true;
m_Animator.SetBool("Loading", Loading); // this will show the panel.
}
public void Hide()
{
Loading = false;
m_Animator.SetBool("Loading", Loading); // this will hide the panel.
}
}
Then in any script which manipulates UI:
public void BuyButtonClicked()
{
Loader.Instance.Show();
// process time taking stuff
Loader.Instance.Hide();
}
You can also create any kind of loading animation as child of panel object using simple images and animation tool inside Unity (for example rotating animation (use fidget spinner, its cool)).
And in case of Android where user have option to leave screen by pressing OS back button you can prevent going back by checking if any loading is in progress by following example:
// code for back button
void Update()
{
if (Input.GetKeyDown(KeyCode.Escape))
{
BackButtonPressed();
}
}
void BackButtonPressed()
{
if(Loader.Instance.Loading)
return;
// use back button event. (For example to leave screen)
}
Hope this helps ;)
I'm trying to create a custom scrollable text area. I created a DrawingArea and a ScrollBar inside a Grid. I have attached the draw event of DrawingArea to this.on_draw method which simply looks at ScrollBar's value and moves the Cairo.Context appropriately before drawing the Pango.Layout.
The first problem is that this.on_draw is getting invoked whenever the ScrollBar is touched even though I have not registered any events with ScrollBar. How do I prevent this, or check this?
The second problem is that even though this.on_draw is invoked, the changes made to the Context is not displayed unless the ScrollBar value is near 0 or 100 (100 is the upper value of Adjustment). Why is this happening?
I did find out that if I connect the value_changed event of ScrollBar to a method that calls queue_redraw of DrawingArea, it would invoke this.on_draw and display it properly after it. But due to the second problem, I think this.on_draw is getting invoked too many times unnecessarily. So, what is the "proper" way of accomplishing this?
using Cairo;
using Gdk;
using Gtk;
using Pango;
public class Texter : Gtk.Window {
private Gtk.DrawingArea darea;
private Gtk.Scrollbar scroll;
private string text = "Hello\nWorld!";
public Texter () {
GLib.Object (type: Gtk.WindowType.TOPLEVEL);
Gtk.Grid grid = new Gtk.Grid();
this.add (grid);
var drawing_area = new Gtk.DrawingArea ();
drawing_area.set_size_request (200, 200);
drawing_area.expand = true;
drawing_area.draw.connect (this.on_draw);
grid.attach (drawing_area, 0, 0);
var scrollbar = new Gtk.Scrollbar (Gtk.Orientation.VERTICAL,
new Gtk.Adjustment(0, 0, 100, 0, 0, 1));
grid.attach (scrollbar, 1, 0);
this.darea = drawing_area;
this.scroll = scrollbar;
this.destroy.connect (Gtk.main_quit);
}
private bool on_draw (Gtk.Widget sender, Cairo.Context ctx) {
ctx.set_source_rgb (0.9, 0.9, 0.9);
ctx.paint ();
var y_offset = this.scroll.get_value();
stdout.printf("%f\n", y_offset);
ctx.set_source_rgb (0.25, 0.25, 0.25);
ctx.move_to(0, 100 - y_offset);
var layout = Pango.cairo_create_layout(ctx);
layout.set_font_description(Pango.FontDescription.from_string("Sans 12"));
layout.set_auto_dir(false);
layout.set_text(this.text, this.text.length);
Pango.cairo_show_layout(ctx, layout);
return false;
}
static int main (string[] args) {
Gtk.init (ref args);
var window = new Texter ();
window.show_all ();
Gtk.main ();
return 0;
}
}
Also, please point out any (possibly unrelated) mistake if you find one in the above code.
The part that you are missing is that a draw signal does not mean "redraw everything". Instead, GTK+ sets the clip region of the cairo context to the part that needs to be redrawn, so everything else you do doesn't have any effect. The cairo function cairo_clip_extents() will tell you what that region is. The queue_draw_area() method on GtkWidget will allow you to explicitly mark a certain area for drawing, instead of the entire widget.
But your approach to scrollbars is wrong anyway: you're trying to build the entire infrastructure from scratch! Consider using a GtkScrolledWindow instead. This automatically takes care of all the details of scrolling for you, and will give you the overlay scrollbars I mentioned. All you need to do is set the size of the GtkDrawingArea to the size you want it to be, and GtkScrolledWindow will do the rest. The best way to do this is to subclass GtkDrawingArea and override the get_preferred_height() and/or get_preferred_width() virtual functions (being sure to set both minimum and natural sizes to the sizes you want for that particular dimension). If you ever need to change this size later, call the queue_resize() method of GtkWidget. (You probably could get away with just using set_size_request(), but what I described is the preferred way of doing this.) Doing this also gives you the advantage of not having to worry about transforming your cairo coordinates; GtkScrolledWindow does this for you.
Hi I'm struggling with this issue.I searched a lot before asking here.
So i have programmed an arcade game which when it dies it pops up a menu with a continue option using one life out of five.Like Subway Surfers with the keys.
The issue is that when i use one heart i want to save it.But everytime i restart the game it keeps saying the initial number which is five.Or without using a life,it by itself,subtracts by one without even clicking the "Save me" button.
Here is my code:
public Text heart;
public Text heart2;
public int counter;
void Start () {
heart.text = "" + counter;
heart2.text = "" +counter;
}
// Update is called once per frame
void Update () {
}
public void SaveMe()
{
heart.text = counter.ToString();
heart2.text = counter.ToString();
int heartScore = PlayerPrefs.GetInt("Life2", 0);
heartScore--;
if(heartScore<0)
{
heartScore = counter;
}
PlayerPrefs.SetInt("Life2", heartScore);
heart.text = PlayerPrefs.GetInt("Life2", 0).ToString();
heart2.text = PlayerPrefs.GetInt("Life2", 0).ToString();
}
}
Then i call this method in the button script.
You should need to save life counter in a file or PlayerPrefs in unity so that loading a scene doesn't effect the counters.
Stores and accesses player preferences between game sessions.
When Unity loads a new scene (if not in an additive way) it will destroy everything that belongs to it before loading the new one. If you don't want a GameObject to be destroyed automatically, read the instructions for DontDestroyOnLoad
Also consider generally to separate your game model (in this occasion the variable counter) to a different Component and use scriptable objects for managing it/them. It will make your life easier in the long run.
I'm learn as3, and building some exercises from the adobe online tutorials. I'm trying to do a very simple task and I think my code is exactly the same as the tutoriala nd it's not working! I just can't work it out...
I'm trying to change the textfield in a button from that buttons class.
The button is called GameButton, the textfield is called blabel and is classic text and dynamic text. Here is the code for the GameButton class. Instead of displaying "Click" as below it just changes the label to the number 1. The trace statement is working etc it is going there, but the text isn't passing through or something. Please help!!!
package {
import flash.display.MovieClip;
public class GameButton extends MovieClip {
public function GameButton() {
trace("Gamebutton has been created");
this.blabel.text = "Click";
stop();
}
}
}
The long and short of it is you can create the button in code, or else you can try listening for added to stage events coming from the parent object you're adding the children to (maybe the stage, maybe another DisplayObjectContainer). The problem with the listening method is I'm not sure how you would know which child just dispatched the event without making some messy code. I think the first option is generally easier and makes more sense, the only caveat is that you have to place the instances using x/y coordinates or apply scaleX, scaleY to stretch or shrink objects instead of doing it using the authoring tool. You can still use the drag and drop parts of flash to figure out coordinates and build individual movie clips etc.
Enough talk on to some code:
package
{
import flash.display.MovieClip;
public class GameButton extends MovieClip {
private var blabel:TextField; //This can be any display object class or a class that extends from a display object class (Sprite, MovieClip, MyCustomButton etc.)
public function GameButton() {
blabel = new TextField(); //The type after new, should be the same, or a sub-class (extension) of the type used in the variable declaration above
addChild(blabel);
//blabel.x=10; //optional over ten pixels from left
//blabel.y=10; //optional down ten pixels from top
//blabel.scaleX=.5; //optional half the width
//blabel.scaleY=2; //optional 2 times taller
trace("Gamebutton has been created");
blabel.text = "Click";
stop();
}
}
}