I currently am attempting to make an app that consists of a line that goes straight up, but when the screen is tapped it changes at a 45 degree angle (To the left or right).
I'm creating the path for this like so:
var path = CGPathCreateMutable()
for var i = 0; i < wayPoints.count; i++ {
let p = wayPoints[i]
if i == 0 {
CGPathMoveToPoint(path, nil, p.x, p.y)
} else {
CGPathAddLineToPoint(path, nil, p.x, p.y)
}
}
And I populate the wayPoints array like so (This chunk of code is called a few times every second):
if (wayPoints.isEmpty) {
wayPoints.append(CGPointMake(0, CGRectGetMinY(self.view!.frame)))
} else {
if (wayPoints.count >= 30) {
while (wayPoints.count >= 30) {
wayPoints.removeAtIndex(0)
}
}
if (currentPosition == Position.Right) {
wayPoints.append(CGPointMake(wayPoints.last!.x + 1, wayPoints.last!.y + 1))
} else if (currentPosition == Position.Left) {
wayPoints.append(CGPointMake(wayPoints.last!.x - 1, wayPoints.last!.y + 1))
} else {
wayPoints.append(CGPointMake(0, wayPoints.last!.y + 1))
}
}
(The currentPosition changes when a tap occurs).
And although I don't think this matters, this is how I'm creating the SKShapeNode line:
let shapeNode = SKShapeNode()
shapeNode.path = path
shapeNode.name = "line"
shapeNode.strokeColor = UIColor.blackColor()
This somewhat works, but for some reason it seems as if there is a "leak" (not memory) in the path. This is what it looks like with the above code:
http://gyazo.com/92dd84de15e125ea3d598cbfa9a86b13
As you can see, it makes almost a triangle when the line turns. It should just be a line without all that excess 'mass' on the sides.
The triangle you see is the line path being filled with a color. In this case, black. To remedy this, set the fillColor property of the shape node to the invisible color by
shapeNode.fillColor = SKColor.clearColor()
I used SKColor here instead of UIColor because the former can be used, without modification, in both Mac OS X and iOS apps.
Related
To avoid being an XY problem, I will talk about some background first.
I am writing the game Planarity as an iOS app. The game starts off with a graph and the player has to move the vertices of the graph so that none of the edges intersect another edge.
I want to color code the edges so that edges that intersect another edge will be in red and those that does not intersect other edges will be in green.
I used a custom UIView called GraphView to display the graph. The GraphView contains subviews of type NodeView, which are used to represent the vertices. In the draw(_:) method of GraphView, I draw the edges between the vertices:
override func draw(_ rect: CGRect) {
// "graph" is the model. It stores where all the vertices are and which vertex is connected to which
if graph == nil { return }
// intersectionDict is of type [Connection: Bool]. It stores whether a Connection intersects with another connection
// value is "true" for a connection that should be red
// and "false" for a connection that should be green
let intersectionDict = graph.checkIntersections()
for connection in graph.connections {
let path = UIBezierPath()
path.lineWidth = 2
path.move(to: CGPoint(x: connection.node1.x, y: connection.node1.y))
path.addLine(to: CGPoint(x: connection.node2.x, y: connection.node2.y))
// assume this is always true, because it is irrelevant
if UserSettings.colorCodeConnections {
if intersectionDict[connection] ?? false {
UIColor.red.setStroke()
} else {
UIColor.green.darker().setStroke()
}
} else {
UIColor.black.setStroke()
}
path.stroke()
}
}
I set up a CADisplayLink to call setNeedsDisplay every frame so that the color of the connections will respond instantly to a user's dragging of the vertices.
When I increase the number of vertices and edges of the graph, the app becomes very laggy. I suppose this is because draw can't return in 1/60 of second (one frame).
I used Instruments to find out that indeed draw is doing a lot of work:
I think I need to make checkIntersection run more quickly.
This is checkIntersections:
func checkIntersections() -> [Connection: Bool] {
var dict = [Connection: Bool]()
var tempConnections = connections // tempConnections is a Set<Connection>
while true {
// find the next connection to check
// we subtract the dictionary's keys here because we don't need to check them (they are already checked)
if let connectionToCheck = tempConnections.subtracting(dict.keys).first {
// get all the connections that connectionToCheck intersects with
let intersections = Set(tempConnections.filter { connectionToCheck.intersects(with: $0) })
if intersections.isEmpty {
dict[connectionToCheck] = false
tempConnections.remove(connectionToCheck)
} else {
dict[connectionToCheck] = true
for connection in intersections {
dict[connection] = true
}
}
} else {
break
}
}
return dict
}
As you can see, I knew that this would take a lot of time when I was writing this algorithm, so I already tried to check as few connections as possible. But since this is so slow, I wonder if I can check even fewer connections. It's ok if this is already the fewest number of checks.
How can I check the fewest connections possible?
EDIT:
Martin R suggested that I should use a nested for loop:
func checkIntersections() -> [Connection: Bool] {
var dict = [Connection: Bool]()
let arrConnections = Array(connections)
for i in 0..<arrConnections.count {
for j in (i+1)..<arrConnections.count {
if arrConnections[i].intersects(with: arrConnections[j]) {
dict[arrConnections[i]] = true
dict[arrConnections[j]] = true
}
}
}
return dict
}
From Instruments, I can see that the "Weight" of the checkIntersections call is about 50%, which is roughly the same as before, when I used sets.
As for the intersects(with:) method, I adapted a solution from another SO answer (that I now can't find), it looks like this:
func intersects(with connection: Connection) -> Bool {
let p1 = self.node1
let p2 = self.node2
let p3 = connection.node1
let p4 = connection.node2
let d = (p2.x - p1.x)*(p4.y - p3.y) - (p2.y - p1.y)*(p4.x - p3.x)
if d == 0 {
return false
}
// if a line starts at where another ends, they don't intersect
// samePointAs just checks whether the two nodes have the same coordinates
if p2.samePointAs(p3) || p4.samePointAs(p1) || p2.samePointAs(p4) || p1.samePointAs(p3){
return false
}
let u = ((p3.x - p1.x)*(p4.y - p3.y) - (p3.y - p1.y)*(p4.x - p3.x))/d
let v = ((p3.x - p1.x)*(p2.y - p1.y) - (p3.y - p1.y)*(p2.x - p1.x))/d
if !(0.0...1.0).contains(u) || !(0.0...1.0).contains(v) {
return false
}
return true
}
The original answer finds the intersection point of two line segments, not just checking whether they intersect. Maybe just checking for intersection can be more easily done than this?
I have created some particles animations with specific sprites which works fine if I use them in the function:
override init(size: CGSize)
I use the following lines:
let sheet_particles = Particles()
let particles_node = SKSpriteNode(texture: sheet_particles.particle000())
particles_node.name = kparticles
particles_node.position = CGPoint(x: 500, y: 500)
particles_node.zPosition = 5
background.addChild(particles_node)
particles_node.runAction(particlesAction)
To make them appear in my scene.
The problem I have is if I try to use them in other functions in my scene, I can not see them.
func panForTranslation(translation : CGPoint) {
let position = selectedNode.position
if selectedNode.name! == kpuzzleNodeName {
selectedNode.position = CGPoint(x: position.x + translation.x * 2, y: position.y + translation.y * 2)
switch selectedNode.name2 {
case "0":
if selectedNode.frame.intersects(NPuzzle13.frame) {
particles_node.position = selectedNode.position
particles_node.runAction(particlesAction)
NPuzzle13.hidden = false
selectedNode.removeFromParent()
}
I see no particles sprite when the condition "0" happens but I see correctly the NPuzzle13. When I check the position of the particles_node node, its position is equal with the node selectedNode. All that is OK, except for the visibility of the particles... What am I missing? Thanks.
About zPosition seems all correct. I dont see any anchorPoint in your code.
I think your switch-case is jumped (not fired, not executed) because you check switch selectedNode.name2 instead of switch selectedNode.name
I have a "U" shaped UIBezierPath which I use as the path for my myImage.layer to animate on. I also have a scrollView. My goal is to have a custom "Pull to Refresh" animation.
The problem I am having is that I want my myImage.layer to update based on how much the scrollView scrolled.
As the scrollView is pulled down, the myImage.layer animates along a "U" shape path. This is the path in my code which I created as a UIBezierPath.
This is how I calculate how far the scrollView is pulled down:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
This is the function to dynamically update the position (it is not working):
func redrawFromProgress(progress: CGFloat) {
// PROBLEM: This is not correct. Only the `x` position is dynamic based on scrollView position.
// The `y` position is static.
// I want this to be dynamic based on how much the scrollView scrolled.
myImage.layer.position = CGPoint(x: progress, y: 50)
}
Basically, this is what I want:
If the scrollView scrolled is 0.0, then the myImage.layer position should be CGPoint(x: 0, y: 0) or the starting point of the path.
If the scrollView scrolled is 0.5 (50%), then the myImage.layer position should be at 50% of the path, I don't know what the CGPoint value would be here.
and so on...
I tried getting the CGPoint values along the UIBezierPath and based on the % of the scrollView scrolled, assign that CGPoint value to it but don't know how to do this. I also looked at this post but I can't get it to work for me.
EDIT QUESTION 1:
By using this extension, I was able to get an array of CGPoints which contain 10 values based on my UIBezierPath:
extension CGPath {
func forEachPoint(#noescape body: #convention(block) (CGPathElement) -> Void) {
typealias Body = #convention(block) (CGPathElement) -> Void
func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
let body = unsafeBitCast(info, Body.self)
body(element.memory)
}
// print(sizeofValue(body))
let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
CGPathApply(self, unsafeBody, callback)
}
func getPathElementsPoints() -> [CGPoint] {
var arrayPoints : [CGPoint]! = [CGPoint]()
self.forEachPoint { element in
switch (element.type) {
case CGPathElementType.MoveToPoint:
arrayPoints.append(element.points[0])
case .AddLineToPoint:
arrayPoints.append(element.points[0])
case .AddQuadCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
case .AddCurveToPoint:
arrayPoints.append(element.points[0])
arrayPoints.append(element.points[1])
arrayPoints.append(element.points[2])
default: break
}
}
return arrayPoints
}
I also rewrote the function above called redrawFromProgress(progress: CGFloat) to this:
func redrawFromProgress(progress: CGFloat) {
let enterPath = paths[0]
let pathPointsArray = enterPath.CGPath
let junctionPoints = pathPointsArray.getPathElementsPoints()
// print(junctionPoints.count) // There are 10 junctionPoints
// progress means how much the scrollView has been pulled down,
// it goes from 0.0 to 1.0.
if progress <= 0.1 {
myImage.layer.position = junctionPoints[0]
} else if progress > 0.1 && progress <= 0.2 {
myImage.layer.position = junctionPoints[1]
} else if progress > 0.2 && progress <= 0.3 {
myImage.layer.position = junctionPoints[2]
} else if progress > 0.3 && progress <= 0.4 {
myImage.layer.position = junctionPoints[3]
} else if progress > 0.4 && progress <= 0.5 {
myImage.layer.position = junctionPoints[4]
} else if progress > 0.5 && progress <= 0.6 {
myImage.layer.position = junctionPoints[5]
} else if progress > 0.6 && progress <= 0.7 {
myImage.layer.position = junctionPoints[6]
} else if progress > 0.7 && progress <= 0.8 {
myImage.layer.position = junctionPoints[7]
} else if progress > 0.8 && progress <= 0.9 {
myImage.layer.position = junctionPoints[8]
} else if progress > 0.9 && progress <= 1.0 {
myImage.layer.position = junctionPoints[9]
}
}
If I pull down the scrollView very slow, the myImage.layer actually follows the path. The only problem is that if I pull down on the scrollView very fast, then the myImage.layer jumps to the last point. Could it be because of the way I wrote the if statement above?
Any ideas?
Thanks to #Sam Falconer for making me aware of this:
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
Once I confirmed this, he also helped by mentioning:
Additionally, you will find the CAKeyframeAnimation class to be useful.
With CAKeyfraneAnimation I am able to manually control it's value with this code:
func scrollViewDidScroll(scrollView: UIScrollView) {
let offsetY = CGFloat(max(-(scrollView.contentOffset.y + scrollView.contentInset.top), 0.0))
self.progress = min(max(offsetY / frame.size.height, 0.0), 1.0)
if !isRefreshing {
redrawFromProgress(self.progress)
}
}
func redrawFromProgress(progress: CGFloat) {
// Animate image along enter path
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = myPath.CGPath
pathAnimation.calculationMode = kCAAnimationPaced
pathAnimation.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
pathAnimation.beginTime = 1e-100
pathAnimation.duration = 1.0
pathAnimation.timeOffset = CFTimeInterval() + Double(progress)
pathAnimation.removedOnCompletion = false
pathAnimation.fillMode = kCAFillModeForwards
imageLayer.addAnimation(pathAnimation, forKey: nil)
imageLayer.position = enterPath.currentPoint
}
Thanks again for the help guys!
Your code is relying on the scrollViewDidScroll delegate callback to be called frequently enough to hit all of your keyframe points. When you pull quickly on the scroll view, it does not call that method frequently enough, causing the jump.
You may want to try calculating a custom path based on a segment of an arc representing the path between your current position, and your desired position. Basing an animation on this, instead of deconstructing your custom path (which looks very close to just being an arc), may be easier.
CGPathAddArc() with x, y, and r being constant, should get you 90% to what your path is now. You could also get fancier with the path to add that line segment like you have at the beginning of your path. It would just take a bit more work to get the partial path to come out right for all the "I'm at this position, get me a path to this other position" logic.
Additionally, you will find the CAKeyframeAnimation class to be useful. You can feed it a CGPath (perhaps one based on the arc segment to travel), and the timing for the animation, and it can make your layer follow the path.
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGPath/index.html#//apple_ref/c/func/CGPathAddArc
Source: https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAKeyframeAnimation_class/index.html
Edit:
Here is some example code for how to draw a partial arc on a CGPath from the current progress to the new progress. I made it work in reverse too. You can play with the numbers and constants, but this is the idea of how to draw an arc segment from a certain percentage to a certain percentage.
Please keep in mind when looking at the CoreGraphics math that it may seem backwards (clockwise vs counterclockwise, etc). This is because UIKit flips everything upside down to put the origin in the upper-left, where CG has its origin in the lower-left.
// start out with start percent at zero, but then use the last endPercent instead
let startPercent = CGFloat(0.0)
// end percent is the "progress" in your code
let endPercent = CGFloat(1.0)
// reverse the direction of the path if going backwards
let clockwise = startPercent > endPercent ? false : true
let minArc = CGFloat(M_PI) * 4/5
let maxArc = CGFloat(M_PI) * 1/5
let arcLength = minArc - maxArc
let beginArc = minArc - (arcLength * startPercent)
let endArc = maxArc + (arcLength * (1.0 - endPercent))
let myPath = CGPathCreateMutable()
CGPathAddArc(myPath, nil, view.bounds.width/2, 0, 160, beginArc, endArc, clockwise)
Here is the full arc segment as defined by the constants minArc and maxArc.
I am trying to implement a graph drawing view in OSX using Cocoa and Quartz framework using NSBezierPath and add/delete data points as I go.
Doing so in drawRect worked fine as the graph was updating frequently but then I encountered performance problem when I need to increase total datapoints/sampling rate.
I decided to move to drawLayer: inContext: but as the function is called at 60fps, the view isn't updating the graph when the function is call and instead update at 1fps.
What am I doing wrong here?
class CustomDrawLayer: CALayer {
convenience init(view: NSView, drawsAsynchronously : Bool = false) {
self.init()
self.bounds = view.bounds
self.anchorPoint = CGPointZero
self.opaque = false
self.frame = view.frame
self.drawsAsynchronously = drawsAsynchronously
// for multiple draws in hosting view
// self.delegate = self
}
override func actionForLayer(layer: CALayer, forKey event: String) -> CAAction? {
return nil
}}
override func drawLayer(layer: CALayer, inContext ctx: CGContext) {
if layer == self.layer {
Swift.print("axes drawing")
graphBounds.origin = self.frame.origin
graphAxes.drawAxesInRect(graphBounds, axeOrigin: plotOrigin, xPointsToShow: CGFloat(totalSecondsToDisplay), yPointsToShow: CGFloat(totalChannelsToDisplay))
}
if layer == self.board {
Swift.print(1/NSDate().timeIntervalSinceDate(fpsTimer))
fpsTimer = NSDate()
drawPointsInGraph(graphAxes, context: ctx)
}
}
func drawPointsInGraph(axes: AxesDrawer, context: CGContext)
{
color.set()
var x : CGFloat = 0
var y : CGFloat = 0
for var channel = 0; channel < Int(totalChannelsToDisplay); channel++ {
path.removeAllPoints()
var visibleIndex = (dirtyRect.origin.x - axes.position.x) / (axes.pointsPerUnit.x / samplingRate)
if visibleIndex < 2 {
visibleIndex = 2
}
for var counter = Int(visibleIndex); counter < dataStream![channel].count; counter++ {
if dataStream![channel][counter] == 0 {
if path.elementCount > 0 {
path.stroke()
}
break
}
let position = axes.position
let ppY = axes.pointsPerUnit.y
let ppX = axes.pointsPerUnit.x
let channelYLocation = CGFloat(channel)
x = position.x + CGFloat(counter-1) * (ppX / samplingRate)
y = ((channelYLocation * ppY) + position.y) + (dataStream![channel][counter-1] * (ppY))
path.moveToPoint(CGPoint(x: align(x), y: align(y)))
x = position.x + CGFloat(counter) * (ppX / samplingRate)
y = ((channelYLocation * ppY) + position.y) + (dataStream![channel][counter] * (ppY) )
path.lineToPoint(CGPoint(x: align(x), y: align(y)))
if x > (axes.position.x + axes.bounds.width) * 0.9 {
graphAxes.forwardStep = 5
dirtyRect = graphBounds
for var c = 0; c < Int(totalChannelsToDisplay); c++ {
for var i = 0; i < Int(samplingRate) * graphAxes.forwardStep; i++
{
dataStream![c][i] = 0
}
}
return
}
}
path.stroke()
}
if inLiveResize {
dirtyRect = graphBounds
} else {
dirtyRect.origin.x = x
dirtyRect.origin.y = bounds.minY
dirtyRect.size.width = 10
dirtyRect.size.height = bounds.height
}
}
It is incredibly rare that you should ever call a function at 60 Hz. In no case should you ever try to call a drawing function at 60 Hz; that never makes sense in Cocoa. If you really mean "at the screen refresh interval," see CADisplayLink, which is specifically built to allow you to draw at the screen refresh interval. This may be slower than 60 Hz. If you try to draw exactly at 60 Hz, you can get out of sync and cause beats in your animation. But this really only intended for things like real-time video. If that what you have, then this is the tool, but it doesn't really sound like it.
It's a bit difficult to understand your code. It's not clear where your 60fps comes in. But I'm assuming what you're trying to do is animate drawing the graph. If so, as Mark F notes, see CAShapeLayer. It has automatic path animations built-in, and is definitely what you want. It automatically handles timings and syncing with the screen refresh and GPU optimizations, and lots of other things that you shouldn't try to work around.
Even if CAShapeLayer isn't what you want, you should be looking at Core Animation, which is designed to work with you to animate values and redraw as necessary. It automatically will handle rendering your layer on multiple cores for instance, which will dramatically improve performance. For more on that, see Animating Custom Layer Properties.
If your path needs to be drawn that frequently, check out CAShapeLayer, where you can just change the path property. That will be hardware accelerated and much faster than drawRect or drawLayer.
I know this question might get asked a lot and for that I am sorry. But I have had trouble with collisions in my game for a while and I would like some help.
First off, the game is a 2D Platformer. Each solid is put in a list. I have this code for collision detection which works pretty good for me:
if (player.rectangle.Intersects(rect))
{
player1Collision = true;
colSolid = solid;
colRectangle = rect;
}
if (player1Collision)
{
Vector2 pos = player.position;
Vector2 pLeft = new Vector2(player.BoundingBox.Left, 0);
Vector2 pRight = new Vector2(player.BoundingBox.Right, 0);
Vector2 pTop = new Vector2(0, player.BoundingBox.Top);
Vector2 pBottom = new Vector2(0, player.BoundingBox.Bottom);
Vector2 sLeft = new Vector2(colSolid.BoundingBox.Left, 0);
Vector2 sRight = new Vector2(colSolid.BoundingBox.Right, 0);
Vector2 sTop = new Vector2(0, colSolid.BoundingBox.Top);
Vector2 sBottom = new Vector2(0, colSolid.BoundingBox.Bottom);
if (player.rectangle.Intersects(colRectangle))
{
if (player.velocity.X > 0 && Vector2.Distance(pRight, sLeft) < player.texture.Width / 2)//left
{
player.velocity.X = 0f;
pos.X = colSolid.BoundingBox.Left - player.BoundingBox.Width;
}
else if (player.velocity.X < 0 && Vector2.Distance(pLeft, sRight) < player.texture.Width / 2)//right
{
player.velocity.X = 0f;
pos.X = colSolid.BoundingBox.Right;
}
if (player.velocity.Y > 0 && Vector2.Distance(pBottom, sTop) < player.texture.Height/ 2) //top
{
player.velocity.Y = 0f;
player.gravityOn = false;
pos.Y = colSolid.BoundingBox.Top - player.BoundingBox.Height;
}
else if (player.velocity.Y < 0 && Vector2.Distance(pTop, sBottom) < player.texture.Height / 2)//bottom
{
player.velocity.Y = 0f;
pos.Y = colSolid.BoundingBox.Bottom;
}
player.position = pos;
}
else
{
player.gravitySpeed = 0.15f;
player.gravityOn = true;
}
}
However the problem is that if the player is not intersecting with the rectangle I set the gravity to on, therefore he falls continuously as he collides with the solid and then is put on top to not collide with it. All I need to know is: how can I avoid this? Is there any other way that I could set the gravity to on without the player falling towards the solid continuously, only to be put back on top of the solid to fall again?
Any help is appreciated.
The way I address this problem may not be optimal (in fact I'm sure it probably isn't) but it has worked for me in all my 2D platforming projects so far.
I begin by defining a second rectangle for the sprite class. This rectangle will have the same Width and X coordinate as the main bounding box, but it will be slightly taller (in my case 2 or 3). You will also need to offset it so that the bottom edges of both rectangles are inline, to illustrate:
Rectangle boundingRect = new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height);
Rectangle gravityRect = new Rectangle((int)boundingRect.X, (int)boundingRect.Y - 3, texture.Width, texture.Height + 3);
The sprite class also needs a bool to keep track of if the player should be falling. And one to keep track of whether it is solid (which you obviously assign as desired, during initialization).
public bool isGrounded = false;
bool isSolid;
At the top of my Game1 class, I declare 2 ints:
int gravityTotalI = 0;
int gravityCounterI = 0;
When initializing my sprites, I usually add them all to a List. So that I can do this:
foreach (Sprite s in spriteList)
{
if (s.isSolid)
{
gravityTotalI++;
}
}
Now, I use this bit of logic in the Game1 Update Method:
foreach (Sprite s in spriteList)
{
if (!s.Equals(player)
{
if (player.boundingRect.Intersects(s.boundingRect) || player.boundingRect.Intersects(s.gravityRect))
{
player.isGrounded = true;
gravityCounterI = 0;
}
else
{
gravCounterI++;
if (gravCounterI >= gravTotalI)
{
player.isGrounded = false;
gravCounterI = 0;
}
}
if (player.boundingRect.Intersects(s.boundingRect))
{
player.position.Y -= 2f; //set the resistance of the platform here
}
}
} //end of foreach loop.
if (!player.isGrounded)
{
player.position.Y += 2f; //set the force of gravity here.
}
Building a decent directional collision engine is a different thing, but this technique will handle the basics (and get rid of that infernal bouncing).
Hope this isn't too long-winded/doesn't miss out anything important, and I really hope it helps - I struggled with exactly the same problem as you for a long time, and I know how frustrating it can be!
I'm looking forward to seeing others' techniques for handling this!