Correctly implementing pan and zoom in libgdx - libgdx

I am almost done with my first libgdx 2d game, only this is missing.
Currently I have my main GameScreen, which contains 3 stages (rendered in this order):
GameStage
Contains game actors, map, etc. Basically the game itself.
InfoStage
All controls and information about the game, so called "hud".
DialogStage
For displaying dialogs so that they are on top of everything.
I am using single OrthographicCamera and viewport of size necessary to display whole map and FitViewport to make sure everything works on resize too or if ratio changes.
Now I would like to make changes so that InfoStage and DialogStage stay pretty much like this - fixed on screen and allow GameStage zooming and pan to move around.
Firstly what does that mean for viewport and camera?
Does that mean I will have camera+viewport pair for InfoStage and DialogStage and different camera + viewport pair for GameStage?
Second and most importantly - how to implement zoom and pan for this stage? I've seen some similar questions, but implementations haven't really worked for me and I also didn't like them.
It feels like there could be simple separated class to do this - maybe GestureListener or EventListener/InputProcessor with passed Camera/Viewport to do all the work automatically.
I found CameraInputController which does something similar so I guess that can be a bit of lead.
Has anyone implemented this component? If not, what would you advise to do?

So far I made GestureListener, which handles zoom and pan, making sure you never zoom out too far and you can never pan out of map, works pretty smooth, but probably doesn't cover all the possibilities like if y axis is rotated.
If you can call fitBounds separately after resize so that world clips in case it needs. I will try to update class as I get it cleaner and more flexible. Pretty sure this scale() method has to have build-in alternative in engine.
Another thing I added is ScrollInputListener, which can listen to mouse scroll and perform zoom.
Just add this class wrapped into GestureDetector and add to your input multiplexer. I am using it with FillViewport.
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.input.GestureDetector
import com.badlogic.gdx.utils.viewport.Viewport
class ScrollInputListener(
val camera: OrthographicCamera,
val panGestureListener: PanGestureListener
) : InputAdapter() {
fun amountToZoom(amount: Int): Float {
return (1.0f + amount.toFloat() / 10f)
}
override fun scrolled(amount: Int): Boolean {
val newZoom = camera.zoom * amountToZoom(amount)
return panGestureListener.doZoomIfAllowed(newZoom)
}
}
class CameraMovement(val x: Float, val y: Float) {
private fun shouldMove(): Boolean {
return x!= 0f || y != 0f
}
fun move(camera: OrthographicCamera, scale: Float): Boolean {
if (shouldMove()) {
camera.translate(-x / scale, y / scale)
return true
}
return false
}
}
val Viewport.targetRatio: Float
get() {
return Gdx.graphics.height.toFloat() / this.worldHeight.toFloat()
}
val Viewport.sourceRatio: Float
get() {
return Gdx.graphics.width.toFloat() / this.worldWidth.toFloat()
}
fun Viewport.scale(): Float {
return if (targetRatio < sourceRatio) Gdx.graphics.width.toFloat() / this.worldWidth.toFloat() else Gdx.graphics.height.toFloat() / this.worldHeight.toFloat()
}
class PanGestureListener(
val camera: OrthographicCamera,
val viewport: Viewport,
val minWorldRatio: Double = 0.5
) : GestureDetector.GestureAdapter() {
override fun pan(x: Float, y: Float, deltaX: Float, deltaY: Float): Boolean {
val scale = viewport.scale()
return fitBounds(scale, deltaX, deltaY).move(camera, scale)
}
fun fitBounds(scale: Float = viewport.scale(), deltaX: Float = 0f, deltaY: Float = 0f): CameraMovement {
val mapLeft = 0
val mapRight = viewport.worldWidth / camera.zoom
val mapTop = 0
val mapBottom = viewport.worldHeight / camera.zoom
val cameraHalfWidth = if (viewport.targetRatio < viewport.sourceRatio) {
camera.viewportWidth / 2f
} else {
camera.viewportWidth / 2f * (viewport.sourceRatio / viewport.targetRatio)
}
val cameraHalfHeight = if (viewport.targetRatio < viewport.sourceRatio) {
camera.viewportHeight / 2f * (viewport.targetRatio / viewport.sourceRatio)
} else {
camera.viewportHeight / 2f
}
var xMovement = deltaX
var yMovement = deltaY
if ((camera.position.x - deltaX / scale) / camera.zoom - cameraHalfWidth < mapLeft) {
camera.position.x = (mapLeft + cameraHalfWidth) * camera.zoom
xMovement = 0f
}
if ((camera.position.x - deltaX / scale) / camera.zoom + cameraHalfWidth > mapRight) {
camera.position.x = (mapRight - cameraHalfWidth) * camera.zoom
xMovement = 0f
}
if ((camera.position.y + deltaY / scale) / camera.zoom - cameraHalfHeight < mapTop) {
camera.position.y = (mapTop + cameraHalfHeight) * camera.zoom
yMovement = 0f
}
if ((camera.position.y + deltaY / scale) / camera.zoom + cameraHalfHeight > mapBottom) {
camera.position.y = (mapBottom - cameraHalfHeight) * camera.zoom
yMovement = 0f
}
return CameraMovement(xMovement, yMovement)
}
var initialZoom = camera.zoom
internal fun checkZoom(newZoom: Float, scale: Float = viewport.scale()): Boolean {
if (
viewport.worldWidth * scale * newZoom > Gdx.graphics.width &&
viewport.worldHeight * scale * newZoom > Gdx.graphics.height
) {
return false
}
if (
viewport.worldWidth * scale * newZoom < Gdx.graphics.width * minWorldRatio &&
viewport.worldHeight * scale * newZoom < Gdx.graphics.height * minWorldRatio
) {
return false
}
return true
}
override fun zoom(initialDistance: Float, distance: Float): Boolean {
val newZoom = initialZoom * initialDistance / distance
val scale = viewport.scale()
return doZoomIfAllowed(newZoom, scale)
}
fun doZoomIfAllowed(newZoom: Float, scale: Float = viewport.scale()): Boolean {
if (checkZoom(newZoom = newZoom, scale = scale)) {
camera.zoom = newZoom
fitBounds(scale = scale)
return true
}
return false
}
override fun touchDown(x: Float, y: Float, pointer: Int, button: Int): Boolean {
initialZoom = camera.zoom
return super.touchDown(x, y, pointer, button)
}
}

Related

Libgdx GestureDirector pan acting strangely when dealing with multiple fingers

I'm creating a game where the movement mechanics are meant to work like this: tap/hold the right side of the screen to accelerate in the direction you're facing and swipe either left or right on the left side of the screen to rotate. It works well enough until the player tries to rotate with a second finger while accelerating with the first. The pan method seems to still run with the second finger but very infrequently and the gdx.input.getX(i) if statements don't fire off. I'm using the pan method of the GestureDetector class. Here's a video: https://www.youtube.com/watch?v=FmHI81ByPmU&feature=youtu.be
I looked at this similar question: libgdx multiple pan events but the answers did not work for me, setting pan to return false did nothing, and the controls aren't related to buttons so I can't change the method into touchDown
The pan method by itself:
#Override
public boolean pan(float x, float y, float deltaX, float deltaY) {
for(int i = 0 ; i < 2 ; i++){//In case they drag wit second finger
if(Gdx.input.isTouched(i) && Gdx.input.getX(i) < Gdx.graphics.getWidth() / 2){
if(Gdx.input.getDeltaX(i) < - 3){
directionListener.onRight();
System.out.println(">");
}
if(Gdx.input.getDeltaX(i) > 3) {
directionListener.onLeft();
System.out.println("<");
}
finger = i;//used to stop body from rotating when the finger rotating it is lifted
System.out.println(i + " = " + Gdx.input.getX(i));
}
}
//Can't replace with touchdown because it does not override method from superclass
return super.pan(x, y, deltaX, deltaY);
}
The entire class:
package com.doppelganger.spacesoccer.Helpers;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.input.GestureDetector;
public class SimpleDirectionGestureDetector extends GestureDetector{
public static int finger = 10;
public interface DirectionListener {
void onLeft();
void onRight();
}
public SimpleDirectionGestureDetector(DirectionListener directionListener) {
super(new DirectionGestureListener(directionListener));
}
private static class DirectionGestureListener extends GestureDetector.GestureAdapter {
DirectionListener directionListener;
DirectionGestureListener(DirectionListener directionListener){
this.directionListener = directionListener;
}
#Override
public boolean pan(float x, float y, float deltaX, float deltaY) {
for(int i = 0 ; i < 2 ; i++){//In case they drag with second finger
if(Gdx.input.isTouched(i) && Gdx.input.getX(i) < Gdx.graphics.getWidth() / 2){
if(Gdx.input.getDeltaX(i) < - 3){
directionListener.onRight();
System.out.println(">");
}
if(Gdx.input.getDeltaX(i) > 3) {
directionListener.onLeft();
System.out.println("<");
}
finger = i;//used to stop body from rotating when the finger rotating it is lifted
System.out.println(i + " = " + Gdx.input.getX(i));
}
}
//Can't replace with touchdown because it does not override method from superclass
return super.pan(x, y, deltaX, deltaY);
}
}
}
I see the problem, you are ignoring the input when you touch the right side of the screen. Here's the line:
if(Gdx.input.isTouched(i) && Gdx.input.getX(i) < Gdx.graphics.getWidth() / 2){
You could verify Gdx.input.getX(i) < Gdx.graphics.getWidth() / 2 inside on another if statement and add an else statement to check the right side, something like this:
if(Gdx.input.isTouched(i)) {
if (Gdx.input.getX(i) < Gdx.graphics.getWidth() / 2) {
// POINTER ON LEFT SIDE - SAME CODE
if(Gdx.input.getDeltaX(i) < - 3) {
directionListener.onRight();
System.out.println(">");
}
if(Gdx.input.getDeltaX(i) > 3) {
directionListener.onLeft();
System.out.println("<");
}
finger = i; //used to stop body from rotating when the finger rotating it is lifted
System.out.println(i + " = " + Gdx.input.getX(i));
} else {
// POINTER ON RIGHT SIDE - NEW CODE
System.out.println("Right side");
}
}

Collision detection for an arc of a circle

So how do i implement the collision detection for an arc of a circle? Will i have to use the Box 2d collision or can i do it some other way using Rectangle or stuff like that?
BTW I hate box2d because i dont understand most of the things in it, so if there is a solution that excludes the box2d, it will be very much appreciated.
The yellow arc keeps on rotating over the black circle. How do i implement collision detection in here?
Please help ! Thanks!
To avoid using Box2D you could define the shape as a polygon and use the polygon.contains(x,y) method or use the Intersector
Below is an example using both:
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Circle;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Polygon;
public class Test extends ApplicationAdapter implements InputProcessor{
private ShapeRenderer sr;
private Polygon polya;
private boolean isColliding = false;
private Circle mp;
#Override
public void create () {
//define arc as polygon
// the more points used to define the shape will
// increase both required computation and collision precision
polya = new Polygon();
// create vertices
float section = 15f;
float[] newVerts = new float[200];
for(int i = 0; i < 50; i++){
newVerts[i*2] = (float)Math.sin(i/section); //x 0 to 98 even
newVerts[i*2+1] = (float)Math.cos(i/section); //y 1 to 99 odd
newVerts[199-i*2] = (float)Math.cos(i/section); //x 100 to 108
newVerts[198-i*2] = (float)Math.sin(i/section) + 0.2f; //y 101 to 199
}
polya.setVertices(newVerts);
polya.scale(50);
polya.setOrigin(1, 1);
polya.rotate(60);
//define circle to act as point for checking intersections
mp = new Circle(Gdx.graphics.getWidth()/2,Gdx.graphics.getHeight()/2,4);
// setup batchers
sr = new ShapeRenderer();
sr.setAutoShapeType(true);
Gdx.input.setInputProcessor(this);
}
#Override
public void render () {
Gdx.gl.glClearColor(0f, 0f, 0f, 0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// check collision with polygon
isColliding = polya.contains(mp.x,mp.y);
//check collision using Intersector
isColliding = Intersector.isPointInPolygon(polya.getTransformedVertices(),0,polya.getVertices().length,mp.x,mp.y);
sr.begin();
sr.setColor(Color.WHITE);
if(isColliding){
sr.setColor(Color.RED);
}
sr.polygon(polya.getTransformedVertices());
sr.circle(mp.x,mp.y,mp.radius);
sr.end();
}
#Override
public void dispose () {
}
#Override
public boolean mouseMoved(int screenX, int screenY) {
int newy = Gdx.graphics.getHeight() - screenY;
polya.setPosition(screenX, newy);
return false;
}
(... removed unused input processor methods for clarity ...)
}
In my case, the arc was in motion and I needed to calculate its collision, so I updated the polygon along with the rendering. Essentially, I got the vertices in the same way that LibGDX renders an arc.
On the left you can see what the arcs I'm drawing look like. On the right you can see what the polygons would look like calculated from the shape of their corresponding arcs if I had to draw them.
Ignore the different colors and count of sections, they are randomly generated.
To achieve this result, I wrote this method:
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Polygon;
import com.badlogic.gdx.math.Vector2;
import java.util.ArrayList;
// ...
public void fillPolygonWithArc(Polygon polygon, float x, float y, float radius, float start, float degrees, int segments) {
float theta = (2 * MathUtils.PI * (degrees / 360.0f)) / segments;
float cos = MathUtils.cos(theta);
float sin = MathUtils.sin(theta);
float cx = radius * MathUtils.cos(start * MathUtils.degreesToRadians);
float cy = radius * MathUtils.sin(start * MathUtils.degreesToRadians);
ArrayList<Vector2> vertices = new ArrayList<>();
vertices.add(new Vector2(x, y));
vertices.add(new Vector2(x + cx, y + cy));
for (int i = 0; i < segments; i++) {
vertices.add(new Vector2(x + cx, y + cy));
float temp = cx;
cx = cos * cx - sin * cy;
cy = sin * temp + cos * cy;
vertices.add(new Vector2(x + cx, y + cy));
}
vertices.add(new Vector2(x + cx, y + cy));
cx = 0;
cy = 0;
vertices.add(new Vector2(x + cx, y + cy));
polygon.setVertices(new float[vertices.size() * 2 + 4]);
for (int i = 0; i < vertices.size(); i++) {
polygon.setVertex(i, vertices.get(i).x, vertices.get(i).y);
}
}
// ...
How does he work?
It takes as parameters a polygon in which to write the vertices of the
arch and the parameters needed to get the shape of the arch, these are
the same parameters that you pass to the method that draws the arc (shapeRenderer.arc).
And then it calculates the vertices of the arch in the same way as
LibGDX does and fills the polygon with them.
I just looked at the LibGDX source.
Use case:
fillPolygonWithArc(polygon, getPosition().x, getPosition().y, getRadius(), start, finalStep, getSegments());
shapeRenderer.setColor(color);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.arc(getPosition().x, getPosition().y, getRadius(), start, finalStep, getSegments());
shapeRenderer.end();
Also, if you want, you can draw a polygon:
fillPolygonWithArc(polygon, getPosition().x, getPosition().y, getRadius(), start, finalStep, getSegments());
shapeRenderer.setColor(color);
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
shapeRenderer.polygon(polygon.getVertices());
shapeRenderer.end();
Ultimately, you can check if some point is inside the polygon you need:
polygon.contains(new Vector2(4, 20));

Trying to create a circle with VBO's - LWJGL

Im trying to create a circle in LWJGL , using VBO's and VAO , and move it using an offset , but it seems one vertex is stuck in the center of the screen . I can't figure out how to move it to the new location . Any help is appreciated , thanks !
P.S : I have already tried debugging the program , but I can't locate the faulty vertex in my array
import java.nio.FloatBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.*;
public class Test {
// Setup variables
private int WIDTH = 800;
private int HEIGHT = 600;
private String title = "Circle";
// Quad variables
private int vbo = 0; // Vertex Buffer Object
private int vao = 0; // Vertex Array Object
int SUBDIVISIONS = 100;
float[] vertex = new float[(SUBDIVISIONS + 1) * 4];
public Test() {
// Initialize
setupOpenGL();
setupQuad();
while (!Display.isCloseRequested()) {
loop();
Display.update();
Display.sync(60);
}
Display.destroy();
}
public void setupOpenGL() {
try {
Display.setDisplayMode(new DisplayMode(WIDTH, HEIGHT));
Display.setTitle(title);
Display.create();
} catch (LWJGLException e) {
e.printStackTrace();
System.exit(-1); // If error , exit program
}
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
}
public void setupQuad() {
float r = 0.2f;
float x;
float y;
float offSetX = 0.3f;
float offSetY = 0.3f;
vertex[0] = (float) Math.sin(Math.PI*2*0/SUBDIVISIONS) * r + offSetX;
vertex[1] = (float) Math.cos(Math.PI*2*1/SUBDIVISIONS) * r + offSetY;
for (int i = 2; i < 360; i = i + 2) {
double angle = Math.PI * 2 * i / SUBDIVISIONS;
x = (float) Math.cos(angle) * r;
vertex[i] = x + offSetX;
}
for (int i = 3; i < 360; i = i + 2) {
double angle = Math.PI * 2 * i / SUBDIVISIONS;
y = (float) Math.sin(angle) * r;
vertex[i] = y + offSetY;
}
FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(vertex.length);
vertexBuffer.put(vertex);
vertexBuffer.flip();
vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
vbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER,vertexBuffer,GL15.GL_STATIC_DRAW);
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 0, 0);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GL30.glBindVertexArray(0);
}
public void loop() {
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
GL30.glBindVertexArray(vao);
GL20.glEnableVertexAttribArray(0);
// Draw the vertices
GL11.glDrawArrays(GL11.GL_TRIANGLE_FAN, 0, vertex.length / 2);
// Put everything back to default (deselect)
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
}
public static void main(String[] args) {
new Test();
}
}
"I think I've found the problem . I was setting the positions of only 359 vertices out of 404 vertices (nr of subdivisions + 1 times 4) . It seems the rest of the vertices were stuck at 0,0 on the screen . Allowing both FOR statements to cycle up to 404 seems to solve the problem"

Dealing with twip precision limit placed on DisplayObject positioning

I'm working on a project to render xps content in flash, and am running into the twip (1/20 of a unit) precision limit in flash. The twip limit is mentioned in the swf file format, but in the flash as3 documentation it's only mentioned in PrintJob.addPage method. However, when setting the DisplayObject x and y properties, any precision less than 0.05 is rounded. Regardless of any scaling. The code below demonstrates this issue.
package
{
import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.display.GraphicsPathCommand;
import flash.display.Shape;
import flash.display.Sprite;
import flash.display.StageScaleMode;
public class TWIPTest extends Sprite
{
// can change behaviour by switching between createPoint and drawPoint
public function TWIPTest()
{
this.stage.scaleMode = StageScaleMode.NO_SCALE;
// scale our unit square so we can see what is happening
scaleX = 400; scaleY = 400;
// draw grid
var grid : Sprite = new Sprite();
addChild(grid);
grid.graphics.lineStyle(TWIP, 0x888888);
drawGrid(1 / TWIP, 1 / TWIP, TWIP, TWIP, grid);
// center of unit square
var cx : Number = 0.5;
var cy : Number = 0.5;
var shape : Shape;
// GREEN in middle of unit square
this.addChild(createPoint(cx, cy, 0x00FF00));
//drawPoint(cx, cy, 0x00FF00, this.graphics);
// BLUE one TWIP away from GREEN
this.addChild(createPoint(cx + TWIP, cy + TWIP, 0x0000FF));
//drawPoint(cx + TWIP, cy + TWIP, 0x0000FF, this.graphics);
// RED half a TWIP away from GREEN (this does not work....)
this.addChild(createPoint(cx + (TWIP / 2), cy + (TWIP / 2), 0xFF0000, 0.5));
//drawPoint(cx + (TWIP / 2), cy + (TWIP / 2), 0xFF0000, this.graphics, 0.5);
// now insert new container to work around limit encontered with RED point
var container : Sprite = new Sprite();
this.addChild(container);
container.scaleX = 0.5; container.scaleY = 0.5;
container.addChild(createPoint(2 * cx + TWIP, 2 * cy + TWIP, 0xFF00FF, 1.0, 2 * TWIP));
//drawPoint(cx + (TWIP / 2), cy + (TWIP / 2), 0xFF00FF, container.graphics);
}
static private function createPoint(x:Number, y:Number,color:uint,alpha:Number=1.0,radius:Number=0.05):Shape
{
var shape : Shape = new Shape();
shape.x = x; shape.y = y;
shape.graphics.beginFill(color,alpha);
shape.graphics.drawCircle(0, 0, radius);
shape.graphics.endFill();
return shape;
}
static private function drawPoint(x:Number, y:Number,color:uint,target:Graphics,alpha:Number=1.0,radius:Number=0.05):void
{
target.beginFill(color,alpha);
target.drawCircle(x, y, radius);
target.endFill();
}
// drawGrid from #Feltope
private function drawGrid(numColumns:Number, numRows:Number, cellHeight:Number, cellWidth:Number, grid:Sprite):void
{
for (var col:Number = 0; col &lt numColumns + 1; col++)
{
for (var row:Number = 0; row &lt numRows + 1; row++)
{
grid.graphics.moveTo(col * cellWidth, 0);
grid.graphics.lineTo(col * cellWidth, cellHeight * numRows);
grid.graphics.moveTo(0, row * cellHeight);
grid.graphics.lineTo(cellWidth * numColumns, row * cellHeight);
}
}
}
static private const TWIP : Number = 0.05;
}
}
This posting also mentions this issue.
The problem is that in parsing xps files this can happen anytime, and many times, for example
&ltCanvas RenderTransform="96.201126,0,0,-95.787476,713.62598,207.05859"&gt
&ltPath Data="..." RenderTransform="0.010394889,0,0,-0.010439778,-7.4180626,2.1616458"&gt
Notice the dx in the Path element, which seems trivial, but the parent Canvas (and other parents above this) scale it up so the rounding of dx to 0 or 0.05 is very noticeable.
I could add Sprite containers whenever this happens, as in the code above, but that's going to bloat memory and slow down rendering.
My question (finally), has anyone dealt with this issue? Any ideas on a better way of handling this limit? Workarounds? Any help appreciated. cheers
what is the biggest problem here? How it looks on screen or that after parsing the precision is lost? You can't do with rendering but you could do modified DisplayObject that will store for example position then reading it will contain the precision kept. or am I missing the point?:)
Edit:
Following is an example of custom display objects (Shape and Sprite)
import flash.display.Shape;
import flash.display.Sprite;
internal class MySprite extends Sprite
{
protected var m_nX:Number;
protected var m_nY:Number;
override public function get x():Number
{
return m_nX;
}
override public function set x(value:Number):void
{
m_nX = value;
super.x = m_nX;
}
override public function get y():Number
{
return m_nY;
}
override public function set y(value:Number):void
{
m_nY = value;
super.y = m_nY;
}
}
internal class MyShape extends Shape
{
protected var m_nX:Number;
protected var m_nY:Number;
override public function get x():Number
{
return m_nX;
}
override public function set x(value:Number):void
{
m_nX = value;
super.x = m_nX;
}
override public function get y():Number
{
return m_nY;
}
override public function set y(value:Number):void
{
m_nY = value;
super.y = m_nY;
}
}
Add this to your example and replace any instantiation of Shape and use MyShape, same for Sprite, replace it by MySprite. There will be no visual difference but the x and y of them will return precise values.
This is a sample test for precision keeping:
addChild(createPoint(0.51000005, 0.510000049, 0x800000, 1, TWIP));
var disp :DisplayObject = getChildAt(numChildren - 1);
trace(disp.x, disp.y);
will trace 0.51000005, 0.510000049 but above test added to your example without MySprite and MyShape custom DisplayObjects will return 0.5 0.5.

Algorithm for particles targeting

I'm building a particles systems, one of the features I'd like to add is a "target" feature. What I want to be able to do is set an X,Y target for each particle and make it go there, not in a straight line though (duh), but considering all other motion effects being applied on the particle.
The relevant parameters my particles have:
posx, posy : inits with arbitrary values. On each tick speedx and speedy are added to posx and posy respectively
speedx, speedy : inits with arbitrary values. On each tick accelx and accely are added to speedx speedy respectively if any)
accelx, accely : inits with arbitrary values. With current implementation stays constant through the lifespan of the particle.
life : starts with an arbitrary value, and 1 is reduced with each tick of the system.
What I want to achieve is the particle reaching the target X,Y on it's last life tick, while starting with it's original values (speeds and accelerations) so the motion towards the target will look "smooth". I was thinking of accelerating it in the direction of the target, while recalculating the needed acceleration force on each tick. That doesn't feel right though, would love to hear some suggestions.
For a "smooth" motion, you either keep the speed constant, or the acceleration constant, or the jerk constant. That depends on what you call "smooth" and what you call "boring". Let's keep the acceleration constant.
From a physics point of view, you have this constraint
targetx - posx = speedx*life + 1/2accelx * life * life
targety - posy = speedy*life + 1/2accely * life * life
Because distance traveled is v*t+1/2at^2. Solving for the unknown acceleration gives
accelx = (targetx - posx - speedx*life) / (1/2 * life * life)
accely = (targety - posy - speedy*life) / (1/2 * life * life)
(For this to work speedy must be in the same unit as time, for example "pixels per tick" and life is a number of "ticks". )
Since you use euler integration, this will not bring the particle exactly on the target. But I doubt it'll be a real issue.
Works like a charm:
Another picture, this time with constant jerk
jerkx = 6.0f*(targetx-x - speedx*life - 0.5f*accelx*life*life)/(life*life*life)
Looks like there is another bend in the curve...
Java code
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
#SuppressWarnings("serial")
public class TargetTest extends JPanel {
List<Particle> particles = new ArrayList<Particle>();
float tx, ty; // target position
public TargetTest() {
tx = 400;
ty = 400;
for (int i = 0; i < 50; i++)
particles.add(new Particle(tx / 2 + (float) (tx * Math.random()), ty / 2
+ (float) (ty * Math.random())));
this.setPreferredSize(new Dimension((int) tx * 2, (int) ty * 2));
}
#Override
protected void paintComponent(Graphics g1) {
Graphics2D g = (Graphics2D) g1;
g.setColor(Color.black);
// comment next line to draw curves
g.fillRect(0, 0, getSize().width, getSize().height);
for (Particle p : particles) {
p.update();
p.draw(g);
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFrame f = new JFrame("Particle tracking");
final TargetTest world = new TargetTest();
f.add(world);
// 1 tick every 50 msec
new Timer(50, new ActionListener() {
#Override
public void actionPerformed(ActionEvent arg0) {
world.repaint();
}
}).start();
f.pack();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}
});
}
class Particle {
float x, y;// position
float vx, vy;// speed
float ax, ay;// acceleration
float jx, jy;// jerk
int life; // life
float lastx, lasty;// previous position, needed to draw lines
int maxlife; // maxlife, needed for color
public Particle(float x, float y) {
this.x = x;
this.y = y;
// pick a random direction to go to
double angle = 2 * Math.PI * Math.random();
setVelocity(angle, 2);// 2 pixels per tick = 2 pixels per 50 msec = 40
// pixels per second
// the acceleration direction 'should' be close to being perpendicular to
// the speed,
// makes it look interesting, try commenting it if you don't believe me ;)
if (Math.random() < 0.5)
angle -= Math.PI / 2;
else
angle += Math.PI / 2;
// add some randomness
angle += (Math.random() - 0.5) * Math.PI / 10;
setAcceleration(angle, 0.1);
life = (int) (100 + Math.random() * 100);
maxlife = life;
lastx = x;
lasty = y;
}
public void setVelocity(double angle, double speed) {
vx = (float) (Math.cos(angle) * speed);
vy = (float) (Math.sin(angle) * speed);
}
public void setAcceleration(double angle, double speed) {
ax = (float) (Math.cos(angle) * speed);
ay = (float) (Math.sin(angle) * speed);
}
#SuppressWarnings("unused")
private void calcAcceleration(float tx, float ty) {
ax = 2 * (tx - x - vx * life) / (life * life);
ay = 2 * (ty - y - vy * life) / (life * life);
}
private void calcJerk(float tx, float ty) {
jx = 6.0f * (tx - x - vx * life - 0.5f * ax * life * life)
/ (life * life * life);
jy = 6.0f * (ty - y - vy * life - 0.5f * ay * life * life)
/ (life * life * life);
}
public void update() {
lastx = x;
lasty = y;
if (--life <= 0)
return;
// calculate jerk
calcJerk(tx, ty);
// or uncomment and calculate the acceleration instead
// calcAcceleration(tx,ty);
ax += jx;
ay += jy;// increase acceleration
vx += ax;
vy += ay;// increase speed
x += vx;
y += vy;// increase position
}
public void draw(Graphics2D g) {
if (life < 0)
return;
g.setColor(new Color(255 - 255 * life / maxlife,
255 * life / maxlife,0));
g.drawLine((int) x, (int) y, (int) lastx, (int) lasty);
}
}
}
You could consider that your particule is initially "applied" a force (Fv) which corresponds to the inertia it has from its initial velocity. Then you apply an attraction force (Fa) that is proportionnal to the distance to the target. You can then sum those forces, and given a particle weight, you can deduce acceleration to consider at time t.
Fa(t) = (Constant / distanceToTarget(t))* [direction to target]
Fv(t) = [initialForce] * dampening(t)
a(t) = (Fa(t) + Fv(t)) / mass
Then you can compute v(t) from v(t-1) and a(t) as usual
Edit: I forgot the life of the particle can directly be computed from the distance to the target (for instance: life = distance / initialDistance will go from 1 at start and approch 0 near the target)
Edit: You could think of this as a kind of magnet. See wikipedia for the force formula.
one kind of movement you can use is the uniform acceleration http://en.wikipedia.org/wiki/Acceleration#Uniform_acceleration
Your particles will make a smoth move towards the target and hit it with rather high velocity
For meeting your stated criteria, do the following:
calculate the distance from the target, the particle will have at the end of it's life time, assuming the speed doesn't change from now on.
this distance put in this equation: http://upload.wikimedia.org/math/6/2/9/6295e1819e6bfe1101506caa4b4ec706.png and solve it for a
use this as your acceleration
Do this seperately for x and y