How do I prevent touches from hitting hidden content in SKCropNode? - objective-c

I'm making pretty heavy use of SKCropNode in my game both for stylistic uses and also in one case where I've built my own SpriteKit version of UIScrollView. I've noticed that when I get a touch event or when a gesture recognizer fires at a certain point, SKScene.nodeAtPoint(...) is still hitting nodes that are hidden at the touch point from the crop node.
How do I prevent SKScene.nodeAtPoint(...) from hitting cropped content, and instead return the next visible node beneath it?

You can't. Everything is based on the frame of the content, and with crop nodes, it is huge. The only thing you can do is check the point of touch, use nodesAtPoint to get all nodes, then enumerate one by one, checking whether or not the touch point is touching a visible pixel by either checking the pixel data, or having a CGPath outlining of your sprite and checking if you are inside that.

I managed to work my way around this one, but it isn't pretty and it doesn't work in every scenario.
The idea is, when a touch is recorded, I iterate over all nodes at that point. If any of those nodes are (or are children of) SKCropNodes, I check the frame of the mask on the crop node. If the touch lies outside the mask, we know the node is not visible at that point and we can assume the touch didn't hit it.
The issue with this method is that I don't do any evaluation of transparency within the crop mask - I just assume it's a rectangle. If the mask is an abnormal shape, I may give false positives on node touches.
extension SKScene {
// Get the node at a given point, but ignore those that are hidden due to SKCropNodes.
func getVisibleNodeAtPoint(point: CGPoint) -> SKNode? {
var testedNodes = Set<SKNode>()
// Iterate over all the nodes hit by this click.
for touchedNode in self.nodesAtPoint(point) {
// If we've already checked this node, skip it. This happens because nodesAtPoint
// returns both leaf and ancestor nodes, and we test the ancestor nodes while
// testing leaf nodes.
if testedNodes.contains(touchedNode) {
continue
}
var stillVisible = true
// Walk the ancestry chain of the target node starting with the touched
// node itself.
var currentNode: SKNode = touchedNode
while true {
testedNodes.insert(currentNode)
if let currentCropNode = currentNode as? SKCropNode {
if let currentCropNodeMask = currentCropNode.maskNode {
let pointNormalizedToCurrentNode = self.convertPoint(point, toNode: currentCropNode)
let currentCropNodeMaskFrame = currentCropNodeMask.frame
// Check if the touch is inside the crop node's mask. If not, we
// know we've touched a hidden point.
if
pointNormalizedToCurrentNode.x < 0 || pointNormalizedToCurrentNode.x > currentCropNodeMaskFrame.size.width ||
pointNormalizedToCurrentNode.y < 0 || pointNormalizedToCurrentNode.y > currentCropNodeMaskFrame.size.height
{
stillVisible = false
break
}
}
}
// Move to next parent.
if let parent = currentNode.parent {
currentNode = parent
} else {
break
}
}
if !stillVisible {
continue
}
// We made it through the ancestry nodes. This node must be visible.
return touchedNode
}
// No visible nodes found at this point.
return nil
}
}

Related

Querying global mouse position in QML

I'm programming a small PoC in QML. In a couple of places in my code I need to bind to/query global mouse position (say, mouse position in a scene or game window). Even in cases where mouse is outside of MouseAreas that I've defined so far.
Looking around, the only way to do it seems to be having whole screen covered with another MouseArea, most likely with hovering enabled. Then I also need to deal with semi-manually propagating (hover) events to underlying mouseAreas..
Am I missing something here? This seems like a pretty common case - is there a simpler/more elegant way to achieve it?
EDIT:
The most problematic case seems to be while dragging outside a MouseArea. Below is a minimalistic example (it's using V-Play components and a mouse event spy from derM's answer). When I click the image and drag outside the MouseArea, mouse events are not coming anymore so the position cannot be updated unless there is a DropArea below.
The MouseEventSpy is taken from here in response to one of the answers. It is only modified to include the position as parameters to the signal.
import VPlay 2.0
import QtQuick 2.0
import MouseEventSpy 1.0
GameWindow {
id: gameWindow
activeScene: scene
screenWidth: 960
screenHeight: 640
Scene {
id: scene
anchors.fill: parent
Connections {
target: MouseEventSpy
onMouseEventDetected: {
console.log(x)
console.log(y)
}
}
Image {
id: tile
x: 118
y: 190
width: 200
height: 200
source: "../assets/vplay-logo.png"
anchors.centerIn: parent
Drag.active: mausA.drag.active
Drag.dragType: Drag.Automatic
MouseArea {
id: mausA
anchors.fill: parent
drag.target: parent
}
}
}
}
You can install a eventFilter on the QGuiApplication, where all mouse events will pass through.
How to do this is described here
In the linked solution, I drop the information about the mouse position when emitting the signal. You can however easily retrieve the information by casting the QEvent that is passed to the eventFilter(...)-method into a QMouseEvent and add it as parameters to the signal.
In the linked answer I register it as singleton available in QML and C++ so you can connect to the signal where ever needed.
As it is provided in the linked answer, the MouseEventSpy will only handle QMouseEvents of various types. Once you start dragging something, there won't be QMouseEvents but QDragMoveEvents e.t.c. Therefore you need to extend the filter method, to also handle those.
bool MouseEventSpy::eventFilter(QObject* watched, QEvent* event)
{
QEvent::Type t = event->type();
if (t == QEvent::MouseButtonDblClick
|| t == QEvent::MouseButtonPress
|| t == QEvent::MouseButtonRelease
|| t == QEvent::MouseMove) {
QMouseEvent* e = static_cast<QMouseEvent*>(event);
emit mouseEventDetected(e->x(), e->y());
}
if (t == QEvent::DragMove) {
QDragMoveEvent* e = static_cast<QDragMoveEvent*>(event);
emit mouseEventDetected(e->pos().x(), e->pos().y());
}
return QObject::eventFilter(watched, event);
}
You can then translate the coordinates to what ever you need to (Screen, Window, ...)
As you have only a couple of places where you need to query global mouse position, I would suggest you to use mapToGlobal or mapToItem methods.
I believe you can get cursor's coordinates from C++ side. Take a look on answer on this question. The question doesn't related to your problem but the solution works as well.
On my side I managed to get global coordinates by directly calling mousePosProvider.cursorPos() without any MouseArea.

XNA 2D object collision (without Tiles/Grid)

First time posting here. Tried to look for topics previously to help.
I'm using Visual Basic, but so far I've been able to follow C# and just translate into VB.
I would like collision without tiles. Smooth movement without any sort of snapping. I already have the movement down, and my sprites stop at the edges of the screen.
I've read I could use Bounds and Intersects, which I have tried. When I apply an IF statement to the arrow keys each time they are pressed, using Bounds and Intersects (I just prevent sprite movement if it is intersecting), it works for ONE key. I move left into an object, and I stop. If I apply the IF to all keys, it will work the first time. Say I move left into an object, the IF statement checks if the Intersects is true or not and acts accordingly.
Now I want to move right, away from the object. I can't since my sprite is ALREADY colliding with the object, since each arrow key is programmed to NOT move if there is Intersection. I see perfectly why this happens.
The code I currently have: (Each arrow key has the same code, altered to it)
If Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Right) And rBlockBounds.X <=
graphics.GraphicsDevice.Viewport.Width - rBlockBounds.Width = True Then
If rBlockBoundBoxBounds.Intersects(rObstructBounds) Then
rBlockBounds.X += 0
rBlockBoundBoxBounds.X = rBlockBounds.X - 1
Else
rBlockBounds.X += 1
rBlockBoundBoxBounds.X = rBlockBounds.X - 1
End If
End If
rBlockBounds is my sprite As Rectangle
rBlockBoundBoxBounds is another Rectangle (1 pixle bigger than rBlockBounds) used as a Hit Box more or less that moves with rBlockBounds, and is the thing doing the collision checking
rObstructBounds is the stationary object that I'm moving my Sprite into.
Anyone have suggestions on how I can make this work?
Since I myself program in C#, not VB I can not code your solution but instead I can explain a better way of approaching it.
What you want to do is prevent the two rectangles from ever intersecting. To do this you will need to implement a move method into your code which can check if the two tiles are colliding. Here is a C# example:
public bool MoveX(float distance) // Move Player Horizontally in this example
{
rBlockBounds.X += distance;
if(rBlockBoundBoxBounds.Intersects(rObstructBounds)
{
rBlockBounds.X -= distance;
return false;
}
return true;
}
Which essentially means that if you run into an object you will be pushed out of it. Since it occurs in one tick you won't get any jutty back-and-front animations.
And that should do what you want. You can test this out and then implement it for y-coordinates as well.
Also, you might notice I've made the function return a bool. This is optional but allows you to check if your player has moved or not.
Note if you teleport an object into another one it will cause problems so remember to implement this every time you move anything.
But that should do what you want.
Edit
Note since your objects are not in a tiled grid, you will need to move lots of time in very small steps.
Something like this:
public bool MoveX(float distance) // Move Player Horizontally in this example
{
rBlockBounds.X += distance;
if(rBlockBoundBoxBounds.Intersects(rObstructBounds)
{
rBlockBounds.X -= distance;
return false;
}
return true;
}
public bool MoveX(float distance, int repeat)
{
for(int i=0; i < repeat; i++)
{
rBlockBounds.X += distance;
if(rBlockBoundBoxBounds.Intersects(rObstructBounds)
{
rBlockBounds.X -= distance;
return false;
}
}
return true;
}
Where the second one will take multiple steps. Here is why you would use it:
MoveX(500); // Will move 500 right. Could easily skip over objects!
MoveX(5, 100); // Will move 5 right one hundred times
// ^ This will take more time but will not skip object
Similarly for yours you could do this:
MoveX(3); // If contact object will be max 3 pixels away
MoveX(1, 3); // If contact object will be max 1 pixels away
MoveX(0.5f, 6); // If contact object will be max 0.5 pixels away
Now I am guessing all your x, y positions are integers. If so you could get away doing the second call and come exactly next to each other. If not you would do the third call.
Hope this helped.

LibGdx Input Handling and Collision Detection

I'm playing with libGdx, creating a simple platformer game. I'm using Tiled to create the map and the LibGdx tiledMap renderer.
It's a similar setup to the SuperKoalio libgdx example.
My Collision detection at the moment, it just determining whether the player has hit a tile to the right of it, above it or below it. When it detects a collision to the right, it sets the players state to standing.
Control of the player is done through the InputHandler. When the D key is pressed, it sets the players state to walking, when the key is released, it sets the state to standing.
My problem is, that if I'm holding down D, and I jump and hit a platform and stop, even when the player has dropped back down and should be able to continue moving, it won't, not until I release the D key and press it again. I can jump fine, but not walk.
Any ideas on why this is and how I can fix it? I've been staring at it for so long that I might be missing something obvious, in which case a fresh pair of eyes might help.
This is the code I've got right at the start of my player.update function to get the player moving.
if(state == State.Standing) {
velocity.x = 0;
} else if(state == State.Walking || state == State.Jumping) {
velocity.x = MAX_VELOCITY;
}
And this is an extract of the collision code :
for (Rectangle tile : tiles) {
if (playerRect.overlaps(tile)) {
state = State.Standing;
break;
}
}
Originally, the collision response set x velocity to 0, and the velocity was used to determine the state, which still produced the same problem.
Thanks
As your Collision-detection is allready working, the thing you need to change is the collision handling.
You set the Players state to Standing.
Instead of doing this you culd set a flag collision and in the update check this flag:
if(state == State.Standing || collision) {
velocity.x = 0;
} else if(state == State.Walking || state == State.Jumping) {
velocity.x = MAX_VELOCITY;
}
This way you know, if you don't move becuase you can't (collision==true) or if you don't move, because you don't press the key (state != State.Standing)
Of course you also need to know, when you don't collide anymore.
For this you could reset the collision flag after setting the velocity and recalculate it the next frame.

Add boundaries to an SKScene

I'm trying to write a basic game using Apple's Sprite Kit framework. So far, I have a ship flying around the screen, using SKPhysicsBody. I want to keep the ship from flying off the screen, so I edited my update method to make the ship's velocity zero. This works most of the time, but every now and then, the ship will fly off the screen.
Here's my update method.
// const int X_MIN = 60;
// const int X_MAX = 853;
// const int Y_MAX = 660;
// const int Y_MIN = 60;
// const float SHIP_SPEED = 50.0;
- (void)update:(CFTimeInterval)currentTime {
if (self.keysPressed & DOWN_ARROW_PRESSED) {
if (self.ship.position.y > Y_MIN) {
[self.ship.physicsBody applyForce:CGVectorMake(0, -SHIP_SPEED)];
} else {
self.ship.physicsBody.velocity = CGVectorMake(self.ship.physicsBody.velocity.dx, 0);
}
}
if (self.keysPressed & UP_ARROW_PRESSED) {
if (self.ship.position.y < Y_MAX) {
[self.ship.physicsBody applyForce:CGVectorMake(0, SHIP_SPEED)];
} else {
self.ship.physicsBody.velocity = CGVectorMake(self.ship.physicsBody.velocity.dx, 0);
}
}
if (self.keysPressed & RIGHT_ARROW_PRESSED) {
if (self.ship.position.x < X_MAX) {
[self.ship.physicsBody applyForce:CGVectorMake(SHIP_SPEED, 0)];
} else {
self.ship.physicsBody.velocity = CGVectorMake(0, self.ship.physicsBody.velocity.dy);
}
}
if (self.keysPressed & LEFT_ARROW_PRESSED) {
if (self.ship.position.x > X_MIN) {
[self.ship.physicsBody applyForce:CGVectorMake(-SHIP_SPEED, 0)];
} else {
self.ship.physicsBody.velocity = CGVectorMake(0, self.ship.physicsBody.velocity.dy);
}
}
}
At first, I used applyImpulse in didBeginContact to push the ship back. This made the ship bounce, but I don't want the ship to bounce. I just want it to stop at the edge.
What is the right way to make the ship stop once it reaches the edge? The code above works most of the time, but every now and then the ship shoots off screen. This is for OS X—not iOS—in case that matters.
Check out this link...
iOS7 SKScene how to make a sprite bounce off the edge of the screen?
[self setPhysicsBody:[SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame]]; //Physics body of Scene
This should set up a barrier around the edge of your scene.
EDIT:
This example project from Apple might also be useful
https://developer.apple.com/library/mac/samplecode/SpriteKit_Physics_Collisions/Introduction/Intro.html
Your code is not clear in what the velocity variables represent. Keep in mind that if the velocity is too high your ship will have travelled multiple points between updates. For example, your ship's X/Y is at (500,500) at the current update. Given a high enough velocity, your ship could be at (500,700) at the very next update. If you had your boundary set at (500,650) your ship would already be past it.
I suggest you do a max check on velocity BEFORE applying it to your ship. This should avoid the problem I outlined above.
As for bouncy, bouncy... did you try setting your ship's self.physicsBody.restitution = 0; ? The restitution is the bounciness of the physics body. If you use your own screen boundaries, then I would recommend setting those to restitution = 0 as well.
Your best bet would be to add a rectangle physics body around the screen (boundary). Set the collision and contact categories of the boundary and player to interact with each other. In the didBeginContact method you can check if the bodies have touched and, if they have, you can call a method to redirect the ship.
Your problem is that your update method may not be checking the location frequently enough before the ship gets off screen.
This will help you to define you screen edges in Swift.
self.physicsBody = SKPhysicsBody ( edgeLoopFromRect: self.frame )

How to bring a SKSpriteNode to front?

How can I bring a SKSpriteNode to the front of all other node?
With UIView, I can use bringSubviewToFront to bring an uiview in front of other views.
You can't "bring it to front" but you can change the zPosition of it.
The higher the zPosition of the node the later it gets rendered.
So if you have three nodes with zPositions of -1, 0 and 1 then the -1 will appear at the back. Then the 0. Then 1 will appear at the front.
If they all use the default zPosition of 0.0 then they are rendered in the order they appear in the children array of the parent node.
You can read it all in the docs.
https://developer.apple.com/documentation/spritekit/sknode
SWIFT 4
I usually create enums to establish zPositions so that I can simply manage the different layers:
enum ZPositions: Int {
case background
case foreground
case player
case otherNodes
}
“case background” is the lower position, it’s equal to the default position of 0;
“case foreground” = 1
“case player” = 2
“case other nodes” = 3
So, when you setup a new item, you can give it the zPosition simply this way:
button.zPosition = CGFloat(ZPosition.otherNodes.rawValue)
(button now has the highest zPosition)
This extension adds a bringToFront method to SKNode. This great for times when zPosition is inappropriate, and you want to rely on sibling ordering.
extension SKNode {
func bringToFront() {
guard let parent = parent else { return }
removeFromParent()
parent.addChild(self)
}
}
`
If you want to bring a node to the very front use this:
yourNode.zPosition = 1
or to the very back:
yourNode.zPosition = -1
or the same level as other nodes(as it's set to this value by default):
yourNode.zPosition = 0