First let me describe what I mean by stutter. When the player moves it looks as if it moves forward a little then back to where it should be and keeps doing it. I am making a small game for learning purposes in lwjgl3 and I am using JOML as my math library. I implemented a fixed time step loop (FPS = 60 and UPS = 30) and I use interpolation to try and smooth my player movement. It works nicely sometimes (not as smooth as I want it though) but other times its just as stuttery as without it. Any ideas on how to fix this? Am I doing the interpolation correctly?
Game Loop:
#Override
public void run() {
window.init("Game", 1280, 720);
GL.createCapabilities();
gameApp.init();
timer.init();
float delta;
float accumulator = 0f;
float interval = 1f / Settings.TARGET_UPS;
float alpha;
while (running) {
delta = timer.getDelta();
accumulator += delta;
gameApp.input();
while (accumulator >= interval) {
gameApp.update();
timer.updateUPS();
accumulator -= interval;
}
alpha = accumulator / interval;
gameApp.render(alpha);
timer.updateFPS();
timer.update();
window.update();
if (Settings.SHOW_PERFORMANCE) {
System.out.println("FPS: " + timer.getFPS() + " UPS: " + timer.getUPS());
}
if (window.windowShouldClose()) {
running = false;
}
}
gameApp.cleanUp();
window.cleanUp();
}
SpriteRenderer:
public class SpriteRenderer {
public StaticShader staticShader;
public SpriteRenderer(StaticShader staticShader, Matrix4f projectionMatrix) {
this.staticShader = staticShader;
staticShader.start();
staticShader.loadProjectionMatrix(projectionMatrix);
staticShader.stop();
}
public void render(Map<TexturedMesh, List<Entity>> entities, float alpha) {
for (TexturedMesh mesh : entities.keySet()) {
prepareTexturedMesh(mesh);
List<Entity> batch = entities.get(mesh);
for (Entity entity : batch) {
Vector2f spritePos = entity.getSprite().getTransform().getPosition();
Vector2f playerPos = entity.getTransform().getPosition();
spritePos.x = playerPos.x * alpha + spritePos.x * (1.0f - alpha);
spritePos.y = playerPos.y * alpha + spritePos.y * (1.0f - alpha);
prepareInstance(entity.getSprite());
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, entity.getSprite().getTexturedMesh().getMesh().getVertexCount());
}
unbindTexturedMesh();
}
}
private void unbindTexturedMesh() {
GL20.glDisableVertexAttribArray(0);
GL20.glDisableVertexAttribArray(1);
GL30.glBindVertexArray(0);
}
private void prepareInstance(Sprite sprite) {
Transform spriteTransform = sprite.getTransform();
Matrix4f modelMatrix = Maths.createModelMatrix(spriteTransform.getPosition(), spriteTransform.getScale(), spriteTransform.getRotation());
staticShader.loadModelMatrix(modelMatrix);
}
private void prepareTexturedMesh(TexturedMesh texturedMesh) {
Mesh mesh = texturedMesh.getMesh();
mesh.getVao().bind();
GL20.glEnableVertexAttribArray(0);
GL20.glEnableVertexAttribArray(1);
GL13.glActiveTexture(GL13.GL_TEXTURE0);
texturedMesh.getTexture().bind();
}
}
EntityPlayer:
public class EntityPlayer extends Entity {
private float xspeed = 0;
private float yspeed = 0;
private final float SPEED = 0.04f;
public EntityPlayer(Sprite sprite, Vector2f position, Vector2f scale, float rotation) {
super(sprite, position, scale, rotation);
this.getSprite().getTransform().setPosition(position);
this.getSprite().getTransform().setScale(scale);
this.getSprite().getTransform().setRotation(rotation);
}
#Override
public void update() {
this.getTransform().setPosition(new Vector2f(this.getTransform().getPosition().x += xspeed, this.getTransform().getPosition().y += yspeed));
}
public void input() {
if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_RIGHT)) {
xspeed = SPEED;
} else if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_LEFT)) {
xspeed = -SPEED;
} else {
xspeed = 0;
}
if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_UP)) {
yspeed = SPEED;
} else if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_DOWN)) {
yspeed = -SPEED;
} else {
yspeed = 0;
}
}
}
Timer:
public class Timer {
private double lastLoopTime;
private float timeCount;
private int fps;
private int fpsCount;
private int ups;
private int upsCount;
public void init() {
lastLoopTime = getTime();
}
public double getTime() {
return GLFW.glfwGetTime();
}
public float getDelta() {
double time = getTime();
float delta = (float) (time - lastLoopTime);
lastLoopTime = time;
timeCount += delta;
return delta;
}
public void updateFPS() {
fpsCount++;
}
public void updateUPS() {
upsCount++;
}
// Update the FPS and UPS if a whole second has passed
public void update() {
if (timeCount > 1f) {
fps = fpsCount;
fpsCount = 0;
ups = upsCount;
upsCount = 0;
timeCount -= 1f;
}
}
public int getFPS() {
return fps > 0 ? fps : fpsCount;
}
public int getUPS() {
return ups > 0 ? ups : upsCount;
}
public double getLastLoopTime() {
return lastLoopTime;
}
}
Your "fixed time step" is not as smooth as you think.
This code:
while (accumulator >= interval) {
gameApp.update();
timer.updateUPS();
accumulator -= interval;
}
may run at 10000000Hz or at 0.1Hz depending on how long gameApp.update()takes to execute.
Edit: You can't take for sure that timer.getDelta() is aproximately the same value each time is called. Same goes for accumulator, which also depends on the remaining value after last -=interval call but starts with a different delta each time.
The OS can take more time for its own proccesses, delaying yours. Sometimes your time-step based on measures may run fine, and the next second it halts for a few milliseconds, enough to mess those measures.
Also, be aware that sending commands to GPU doesn't guarantee they get processed immediately; perhaps they accumulate and later run all in a row.
If you wish some code to be executed every M milliseconds (e.g. 16.6ms for 60 FPS) then use a Timer and scheduleAtFixedRate(). See this
The next issue you must deal with is that rendering must be done in a shorter time than the fixed step, or else some delay appears. To achieve this goal send to the GPU most of data (vertices, textures, etc) just once. And for each frame render send only the updated data (the camera position, or just a few objects).
Related
I'm writing a program that deals with ball/particle movements. I want to program the logic so "hot" balls move between 4cm and 6cm per sec, while "cold" balls move between 2cm and 4cm per sec. How can I use the pixels per cm (113/2.54), where 113 is my screen resolution per inch, to set the velocities?
I'm just using a fixed number for the vx and vy values for the balls.
This is my Ball constructor
public Ball(Side s, Color color) { //Side character used to determine if ball should spawn on left or right side.
//makeRandom method gives positive or negative direction for each ball
if (color == Color.RED){
this.vx = 8 * makeRandom();
this.vy = 8 * makeRandom();
} else {
this.vx = 5 * makeRandom();
this.vy = 5 * makeRandom();
}
speed = Math.sqrt(Math.pow(this.vx, 2)+Math.pow(this.vy, 2));
//position is randomized for each ball
if(s == Side.LEFT) {
this.x = leftSideBallX + (int) (50 * Math.random());
this.y = leftSideBallY + (int) (50 * Math.random());
} else {
this.x = rightSideBallX + (int) (50 * Math.random());
this.y = rightSideBallY + (int) (50 * Math.random());
}
}
and this is my game loop
#Override
public void run() {
running = true;
addParticlesToVector();
long start;
long elapsed;
long wait;
while (running) {
start = System.nanoTime();
gameUpdate();
this.repaint();
leftTemp.setText("Temperature: " + df.format(lTemp));
rightTemp.setText("Temperature: " + df.format(rTemp));
elapsed = (start - System.nanoTime());
wait = targetTime - elapsed/1000000;
try {
Thread.sleep(wait);
} catch (Exception e) {
e.printStackTrace();
}
}
}
If you truly want the velocity to be in a range (rather than the x and y velocities) then you'll need to separately generate speed and direction. At the moment you generate the x and y speeds independently which will give you a much larger range of combined velocities. This needs some basic trig.
I suggest keeping the model's values separate from the user interface considerations (such as converting from cms to #pixels. I also suggest encapsulating your concept of heat.
class Ball {
private float angle;
private float speed;
public Ball(Random random, Heat heat) {
this.angle = random.nextFloat(Math.PI * 2);
this.speed = heat.randomSpeed(random);
}
public float xVelocity() {
return speed * Math.cos(angle);
}
public float yVelocity() {
return speed * Math.sin(angle);
}
}
class ScreenConverter {
private static final float PIXELS_PER_CM = 113f/2.54f;
public float pixelsToDistance(int pixels) {
return pixels * PIXELS_PER_CM;
}
public int distanceToPixels(float distanceInCMs) {
return int(distanceInCM / PIXELS_PER_CM);
}
}
I'm making a small asteroids game, and I'm having some trouble controlling the animation speed.
For example, let's say I have 20 asteroids in my game, when I destroy an asteroid, the amount of asteroids goes down (obviously). Because there are fewer objects in the game, the fps goes up and the animation speed of the asteroids is getting faster and faster.
I fixed it by adjusting the animation speed according to the amount of asteroids I have in the game, but I'm also facing another problem with the explosions when I destroy an asteroid. I could do the same thing I did with the asteroids I suppose, but I just think it's not a very wise way to "solve" it and just seems like bad practice to me.
I thought of capping the fps, but I'm not really sure how to do it. I'd like to get some advices and what's the best way to deal with such situations.
I'll post here my main game class including the game loop, and an example of the explosion class so you'll get the general idea of the code.
Game class and loop:
import com.asteroids.view.*;
public class Game extends Canvas implements Runnable {
private static final long serialVersionUID = -8921419424614180143L;
public static final int WIDTH = 1152, HEIGHT = WIDTH / 8 * 5;
private Thread thread;
private boolean isRunning;
private LoadImages loadImages = new LoadImages();
private Player player = new Player();
private AllObjects objects;
private KeyInput keyInput;
private long delay = 80;
private long currentTime = System.currentTimeMillis();
private long expectedTime = currentTime + delay;
public static BufferedImage test;
public Game() {
new Window(WIDTH, HEIGHT, "Asteroids!", this);
objects = new AllObjects();
objects.addObject(player);
for (int i = 0; i < 20; i++) {
objects.addObject(new Rock((int) (Math.random() * (Game.WIDTH - 64) + 1),
(int) (Math.random() * (Game.HEIGHT - 64) + 1)));
}
keyInput = new KeyInput(player);
this.addKeyListener(keyInput);
}
public void run() {
this.requestFocus();
long lastTime = System.nanoTime();
double amountOfTicks = 60.0;
double ns = 1000000000 / amountOfTicks;
double delta = 0;
long timer = System.currentTimeMillis();
int frames = 0;
// main game loop.
while (isRunning) {
adjustAsteroidsSpeed();
destroyAsteroids();
collisionLoop();
// used to set delay between every bullet(milliseconds)
currentTime = System.currentTimeMillis();
if (KeyInput.shoot && currentTime >= expectedTime) {
// calculates the accurate position of the x,y on the "circumference" of the
// player
float matchedX = player.getX() + 1 + (float) ((player.getRadius() + 32) * Math.cos(player.getRadian()));
float matchedY = player.getY() - 7 + (float) ((player.getRadius() + 32) * Math.sin(player.getRadian()));
objects.addObject(new Bullet(matchedX, matchedY, player));
expectedTime = currentTime + delay;
}
destroyBullets();
long now = System.nanoTime();
delta += (now - lastTime) / ns;
lastTime = now;
while (delta >= 1) {
tick();
delta--;
}
if (isRunning)
render();
frames++;
if (System.currentTimeMillis() - timer > 1000) {
timer += 1000;
System.out.println("FPS: " + frames);
frames = 0;
}
}
render();
stop();
System.exit(1);
}
private void stop() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.exit(1);
}
private void render() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
this.createBufferStrategy(3);
return;
}
Graphics g = bs.getDrawGraphics();
g.drawImage(LoadImages.getbackground(), 0, 0, getWidth(), getHeight(), this);
objects.render(g);
player.render(g);
g.dispose();
bs.show();
}
private void tick() {
player.tick();
objects.tick();
}
// starting thread and game loop.
public void start() {
thread = new Thread(this);
thread.start();
isRunning = true;
}
// minimum and maximum possible position for object.
public static float Bounds(float value, float min, float max) {
if (value >= max) {
return value = max;
}
if (value <= min) {
return value = min;
} else {
return value;
}
}
// detects collision between two objects
public boolean collision(GameObject a, GameObject b) {
return (b.getX() - a.getX() + 10) * (b.getX() - a.getX() + 10)
+ (b.getY() - a.getY() + 10) * (b.getY() - a.getY() + 10) < (a.getRadius() + b.getRadius())
* (a.getRadius() + b.getRadius());
}
// destroys bullets once they go out of the screen
public void destroyBullets() {
for (int i = 0; i < objects.getSize(); i++) {
if (objects.get(i).getId() == ID.BULLET) {
GameObject bullet = objects.get(i);
if (bullet.getX() > Game.WIDTH || bullet.getX() < 0 || bullet.getY() > Game.HEIGHT
|| bullet.getY() < 0) {
objects.removeObject(bullet);
}
}
}
}
// whenever a collision between an asteroid and a bullet occurs, the asteroid and the bullets are destroyed
public void destroyAsteroids() {
GameObject bullet = null;
GameObject bigRock = null;
for (int i = 0; i < objects.getSize(); i++) {
if (objects.get(i).getId() == ID.BULLET) {
bullet = (Bullet) objects.get(i);
for (int q = 0; q < objects.getSize(); q++) {
if (objects.get(q).getId() == ID.BIGROCK) {
bigRock = objects.get(q);
if (bullet != null && bigRock != null) {
if (collision(bigRock, bullet)) {
objects.addObject(new Explosion(bigRock.getX(), bigRock.getY(), objects));
objects.removeObject(bigRock);
objects.removeObject(bullet);
}
}
}
}
}
}
}
// calculates the amount of asteroids in the game and adjust the asteroids speed
public void adjustAsteroidsSpeed() {
int rocksCount = 0;
Rock rock;
for (GameObject object : objects.link()) {
if (object.getId() == ID.BIGROCK) {
rocksCount++;
}
}
for (GameObject object : objects.link()) {
if (object.getId() == ID.BIGROCK) {
rock = (Rock) object;
rock.setAnimSpeed(rocksCount * 0.002f);
}
}
}
Explosion class:
package com.asteroids.model;
import java.awt.Graphics;
import java.awt.Image;
import com.asteroids.controller.*;
import com.asteroids.view.LoadImages;
public class Explosion extends GameObject {
private AllObjects objects;
private Image explosion;
private float frame = 0;
private float animSpeed = 0.09f;
private int frameCount = 48;
public Explosion(float x, float y, AllObjects objects) {
super(x, y, ID.EXPLOSION, 1);
this.objects = objects;
}
public void render(Graphics g) {
explosion(g);
}
public void explosion(Graphics g) {
frame += animSpeed;
if (frame > frameCount) {
frame -= frameCount;
}
explosion = LoadImages.getExplosion().getSubimage((int) frame * 256, 0, 256, 256);
g.drawImage(explosion, (int) x, (int) y, 110, 110, null);
if (frame >= 47.8f) {
objects.removeObject(this);
}
}
public void tick() {
}
public void setAnimSpeed(float animSpeed) {
this.animSpeed = animSpeed;
}
}
Your main loop is generating uneven updates. If I do nothing, I get anywhere between 7799913 and 8284754 fps, however, if I throw in a 8 millisecond delay (to simulate some work), it drops to around 115-120 fps.
Your intention is to try and get the frame rate to be as even as possible, this will ensure that the animation speed remains the same
Personally, I don't like the "free-wheeling" style of game loop, it means that the loop is been allowed to consume CPU cycles without actually doing anything, where those cycles could be been used to do more important work, like update the UI.
In most cases, I just use a Swing Timer set to something like 5 millisecond intervals and then make use of the date/time API to calculate the difference between now and the last update and make choices about what to do, but, this assumes you're using a Swing based painting path. If you're doing a direct painting path (ie BufferStrategy), you could use a similar idea with a "loop" instead...
public void run() throws InterruptedException {
int frames = 0;
Duration threashold = Duration.ofMillis(1000 / 59);
Duration cycle = Duration.ofSeconds(1);
Instant cycleStart = Instant.now();
// main game loop.
while (isRunning) {
Instant start = Instant.now();
// Some update function...
Thread.sleep(rnd.nextInt(32));
Duration processTime = Duration.between(start, Instant.now());
Duration remainingTime = threashold.minusMillis(processTime.toMillis());
long delay = remainingTime.toMillis();
if (delay > 0) {
Thread.sleep(delay);
} else {
System.out.println("Dropped frame");
}
frames++;
// Render the output
Duration cycleTime = Duration.between(cycleStart, Instant.now());
if (cycleTime.compareTo(cycle) >= 0) {
cycleStart = Instant.now();
System.out.println(frames);
frames = 0;
}
}
}
In this example, your update and paint scheduling code simply have 16 milliseconds to get there job done, otherwise it will drop frames. If the work takes less then 16 milliseconds, the loop will "wait" the remaining time in order to provide some breathing room for the CPU to give time to other threads (and not take update unnecessary time on the CPU)
In the example above, I generate a "random" delay of up to 32 milliseconds for testing. Set it back to 16 and you should get (roughly) 60fps.
Now, I know people are extraordinarily passionate about these things, so if using Thread.sleep and Duration make your skin crawl, you "could" use a "free wheeling" loop, something like the one presented in Java Main Game Loop
Below is a sample implementation, I've set the number of updates and frames per second to 60, but you can change those values to suit your needs...
public void run() throws InterruptedException {
double ups = 60;
double fps = 60;
long initialTime = System.nanoTime();
final double timeU = 1000000000 / ups;
final double timeF = 1000000000 / fps;
double deltaU = 0, deltaF = 0;
int frames = 0, ticks = 0;
long timer = System.currentTimeMillis();
while (isRunning) {
long currentTime = System.nanoTime();
deltaU += (currentTime - initialTime) / timeU;
deltaF += (currentTime - initialTime) / timeF;
initialTime = currentTime;
if (deltaU >= 1) {
Thread.sleep(rnd.nextInt(32));
//getInput();
//update();
ticks++;
deltaU--;
}
if (deltaF >= 1) {
Thread.sleep(rnd.nextInt(32));
//render();
frames++;
deltaF--;
}
if (System.currentTimeMillis() - timer > 1000) {
System.out.println(String.format("UPS: %s, FPS: %s", ticks, frames));
frames = 0;
ticks = 0;
timer += 1000;
}
}
}
Again, the Thread.sleep here is just to inject a random amount of "work". Because it allows for more then 16ms of delay, you will also find it "drops" frames. Your job would be to get you work down to under 16ms per pass
Currently I'm working on a simple 2d platformer, and I decided to work on physics before working on the general concept of the game. I actually haven't learned physics in school or anything, so I'm just using google/youtube tutorials as my main resources. I've currently gotten jumping working pretty nicely, but moving side to side isn't what I would like it to be. I want it to use acceleration/deceleration vs. just incrementing/decrementing the x position by a constant speed. Now I've tried using this website for x motion but that seems to be what I'm using for jumping. Here is my current player class:
import game.Game;
import game.input.PlayerController;
public class Player {
private float dt = 0.18F, gravity = 9.81F;
public float x, y, dx, dy;
private PlayerController controller;
private boolean jumping, onGround;
public float speed = 7.5F;
public float jh = 60F;
public float vy = 120F;
public Player() {
controller = new PlayerController();
}
public void update() {
controller.update(this);
dy += gravity * dt * (jumping ? -1F : 1F);
if(!jumping && !onGround && dy > vy) dy = vy;
y += dy * dt + 0.5F * gravity * dt * dt;
x += dx;
if (y > Game.height - 32) {
dy = 0;
y = Game.height - 32;
onGround = true;
} else
onGround = false;
if(dy < jh) { jumping = false;}
dx = 0;
}
public void render(Graphics g) {
g.setColor(Color.red);
g.fillRect(x, y, 32, 32);
}
public void moveLeft() {
dx -= speed;
}
public void moveRight() {
dx += speed;
}
public void jump() {
if (onGround) {
jumping = true;
dy -=jh;
}
}
}
In the PlayerController class I'm just calling player.moveLeft() and player.moveRight() when the left and right keys are pressed.
If anybody has a good idea on how to make smoother movement, that would be very helpful. Thanks!
I'm working in a spaceship first person view game. I have a joystick, and when i move the joystick i can move all the objects (asteroids) of the screen simulating that the spaceship is being moved with the joystick.
The game works fine, but now i have a problem. If you are pressing the joystick in the max left position and then you do ACTION_UP and then instantly ACTION_DOWN in the joystick again but in the max right position, the spaceship starts moving to the right at max speed. It is hard to explain it. For example, If you press the joystick in max left position the spaceship is moving -20px per frame to the left and if you press the joystick in the max right position, the spaceship moves to the right +20px per frame.
So, now, if i do a fast max left and max right touch on the joystick, the spaceship does this movement: -20....+20
It is not reallistic movement.
I want to get this movement: -20 -17 -14 -9 -5 0 +5 +9 +14 +17 +20.... I mean a more reallistic spaceship movement. But the problem is that i am not a math or physics expert, and i dont have any idea of how to get that kind of functionality in this joystick... any help will be very grateful.
Here you can find a demo project with the joystick: https://mega.co.nz/#!cp5FhYIT!dM88qx_xQdyhED9fX_4xeJ9ciQYJirUlNzEi-KOzU2k
This is the joystick code, i found it in google and works very well except for the non realistic movement that i described before:
public class Joystick extends View {
public static final int INVALID_POINTER = -1;
private JoystickMovedListener moveListener;
//# of pixels movement required between reporting to the listener
private float moveResolution;
//Max range of movement in user coordinate system
private float movementRange;
//Last touch point in view coordinates
private int pointerId = INVALID_POINTER;
private float touchX;
private float touchY;
private float touchXDelayedMovement;
private float touchYDelayedMovement;
//Handle center in view coordinates
private float handleX;
private float handleY;
//Last reported position in view coordinates (allows different reporting sensitivities)
private float reportX;
private float reportY;
//Center of the view in view coordinates
private int cX;
private int cY;
//Size of the view in view coordinates
private int dimX;
private int dimY;
private int innerPadding;
private int bgRadius;
private int handleRadius;
private int movementRadius;
private int handleInnerBoundaries;
//Cartesian coordinates of last touch point - joystick center is (0,0)
private int cartX;
private int cartY;
//User coordinates of last touch point
private int userX;
private int userY;
//Offset co-ordinates (used when touch events are received from parent's coordinate origin)
private int offsetX;
private int offsetY;
private Paint bgPaint;
private Paint handlePaint;
boolean disabled;
Handler handler;
Handler handlerDelayedMovement;
public Joystick(Context context) {
super(context);
initJoystickView();
}
private void initJoystickView() {
setFocusable(true);
handlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
handlePaint.setColor(Color.RED);
handlePaint.setStrokeWidth(1);
handlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setColor(Color.DKGRAY);
bgPaint.setStrokeWidth(1);
bgPaint.setStyle(Paint.Style.FILL_AND_STROKE);
this.moveResolution = 1.0f;
handler = new Handler();
handlerDelayedMovement = new Handler();
}
public void setMovementRange(float movementRange) {
this.movementRange = movementRange;
}
public void setOnJostickMovedListener(JoystickMovedListener listener) {
this.moveListener = listener;
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
int d = Math.min(getMeasuredWidth(), getMeasuredHeight());
dimX = d;
dimY = d;
cX = d / 2;
cY = d / 2;
bgRadius = dimX/2 - innerPadding;
handleRadius = (int)(d * 0.2);
handleInnerBoundaries = handleRadius;
movementRadius = Math.min(cX, cY) - handleInnerBoundaries;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Here we make sure that we have a perfect circle
int measuredWidth = measure(widthMeasureSpec);
int measuredHeight = measure(heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
}
private int measure(int measureSpec) {
int result = 0;
// Decode the measurement specifications.
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.UNSPECIFIED) {
result = 200; // Return a default size of 200 if no bounds are specified.
} else {
result = specSize; // As you want to fill the available space always return the full available bounds.
}
return result;
}
#Override
protected void onDraw(Canvas canvas) {
canvas.save();
// Draw the background
canvas.drawCircle(cX, cY, bgRadius, bgPaint);
// Draw the handle
handleX = touchX + cX;
handleY = touchY + cY;
canvas.drawCircle(handleX, handleY, handleRadius, handlePaint);
canvas.restore();
}
public void setPointerId(int id) {
this.pointerId = id;
}
public int getPointerId() {
return pointerId;
}
#Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
if (disabled==true)
break;
return processMoveEvent(ev);
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if ( pointerId != INVALID_POINTER ) {
returnHandleToCenter();
returnHandleToCenterDelayedMovement();
setPointerId(INVALID_POINTER);
}
break;
}
case MotionEvent.ACTION_POINTER_UP: {
if ( pointerId != INVALID_POINTER ) {
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if ( pointerId == this.pointerId ) {
returnHandleToCenter();
returnHandleToCenterDelayedMovement();
setPointerId(INVALID_POINTER);
return true;
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
handlerDelayedMovement.removeCallbacksAndMessages(null);
if ( pointerId == INVALID_POINTER ) {
int x = (int) ev.getX();
if ( x >= offsetX && x < offsetX + dimX ) {
setPointerId(ev.getPointerId(0));
if (disabled==true){
return true;
}
return processMoveEvent(ev);
}
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
if ( pointerId == INVALID_POINTER ) {
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
int x = (int) ev.getX(pointerId);
if ( x >= offsetX && x < offsetX + dimX ) {
setPointerId(pointerId);
return true;
}
}
break;
}
}
return false;
}
private boolean processMoveEvent(MotionEvent ev) {
if ( pointerId != INVALID_POINTER ) {
final int pointerIndex = ev.findPointerIndex(pointerId);
// Translate touch position to center of view
float x = ev.getX(pointerIndex);
touchX = x - cX - offsetX;
float y = ev.getY(pointerIndex);
touchY = y - cY - offsetY;
reportOnMoved();
invalidate();
return true;
}
return false;
}
private void reportOnMoved() {
//constraint circle
float diffX = touchX;
float diffY = touchY;
double radial = Math.sqrt((diffX*diffX) + (diffY*diffY));
if ( radial > movementRadius ) {
touchX = (int)((diffX / radial) * movementRadius);
touchY = (int)((diffY / radial) * movementRadius);
}
//We calc user coordinates
//First convert to cartesian coordinates
cartX = (int)(touchX / movementRadius * movementRange);
cartY = (int)(touchY / movementRadius * movementRange);
//Cartesian Coordinates
userX = cartX;
userY = cartY;
if (moveListener != null) {
boolean rx = Math.abs(touchX - reportX) >= moveResolution;
boolean ry = Math.abs(touchY - reportY) >= moveResolution;
if (rx || ry) {
this.reportX = touchX;
this.reportY = touchY;
moveListener.OnMoved(userX, userY);
}
}
}
private void reportOnMovedDelayedMovement() {
//constraint circle
float diffX = touchXDelayedMovement;
float diffY = touchYDelayedMovement;
double radial = Math.sqrt((diffX*diffX) + (diffY*diffY));
if ( radial > movementRadius ) {
touchXDelayedMovement = (int)((diffX / radial) * movementRadius);
touchYDelayedMovement = (int)((diffY / radial) * movementRadius);
}
//We calc user coordinates
//First convert to cartesian coordinates
cartX = (int)(touchXDelayedMovement / movementRadius * movementRange);
cartY = (int)(touchYDelayedMovement / movementRadius * movementRange);
//Cartesian Coordinates
userX = cartX;
userY = cartY;
if (moveListener != null) {
boolean rx = Math.abs(touchXDelayedMovement - reportX) >= moveResolution;
boolean ry = Math.abs(touchYDelayedMovement - reportY) >= moveResolution;
if (rx || ry) {
this.reportX = touchXDelayedMovement;
this.reportY = touchYDelayedMovement;
moveListener.OnMoved(userX, userY);
}
}
}
private void returnHandleToCenter() {
final int numberOfFrames = 5;
final double intervalsX = (0 - touchX) / numberOfFrames;
final double intervalsY = (0 - touchY) / numberOfFrames;
handler.removeCallbacksAndMessages(null);
for (int i = 0; i < numberOfFrames; i++) {
final int j = i;
handler.postDelayed(new Runnable() {
#Override
public void run() {
touchX += intervalsX;
touchY += intervalsY;
//reportOnMoved();
invalidate();
if (moveListener != null && j == numberOfFrames - 1) {
moveListener.OnReturnedToCenter();
}
}
}, i * 10);
}
if (moveListener != null) {
moveListener.OnReleased();
}
}
private void returnHandleToCenterDelayedMovement() {
final int numberOfFrames = 25;
touchXDelayedMovement=touchX;
touchYDelayedMovement=touchY;
final double intervalsX = (0 - touchXDelayedMovement) / numberOfFrames;
final double intervalsY = (0 - touchYDelayedMovement) / numberOfFrames;
handlerDelayedMovement.removeCallbacksAndMessages(null);
for (int i = 0; i < numberOfFrames; i++) {
handlerDelayedMovement.postDelayed(new Runnable() {
#Override
public void run() {
touchXDelayedMovement += intervalsX;
touchYDelayedMovement += intervalsY;
reportOnMovedDelayedMovement();
}
}, i * 50);
}
}
public void setInnerPadding(int innerPadding){
this.innerPadding=innerPadding;
}
public void disable(){
disabled=true;
}
public void enable(){
disabled=false;
}
public interface JoystickMovedListener {
public void OnMoved(int pan, int tilt);
public void OnReleased();
public void OnReturnedToCenter();
}
}
You must do this in the class that will use the joystick:
private JoystickMovedListener joystickListener = new JoystickMovedListener() {
#Override
public void OnMoved(int pan, int tilt) {
//here i move the objects in the game
}
}
#Override
public void OnReleased() {}
public void OnReturnedToCenter() {};
};
joystickOnScreen = new Joystick(this);
joystickOnScreen.setMovementRange(screenHeight/50);
joystickOnScreen.setInnerPadding(screenHeight/30);
joystickOnScreen.setOnJostickMovedListener(joystickListener);
RelativeLayout.LayoutParams joystickParams = new RelativeLayout.LayoutParams(sh/3, sh/3);
joystickParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
joystickParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
joystickParams.setMargins(sh/100, 0, 0, sh/100);
joystickOnScreen.setLayoutParams(joystickParams);
joystickOnScreen.setAlpha(0.3f);
I will not implement the changes for you but hopefully this answer can help you towards implementing this on your own.
With your current implementation you are updating the object position (x, y) each frame. To get the more realistic physics that you want, you need to store and update velocity as well (vx, vy).
Add two new variables, vx and vy (with initial values of zero) in the objects that you are currently updating the position for. The joystick should control the change of the velocity instead of the position. Change the code that updates the positions x and y, to update the velocities vx and vy instead. When the joystick is max left, you can for example set vx = vx - 3.
After the velocity is updated, you need to update the position using the velocity variables. For example, set the position x = x + vx. Ideally you want this to happen in a different method that runs even if you don't move the joystick, but to keep it simple you can do this update right after the update of the velocity variables.
With this implementation you will get a more realistic game physics. As a next step you might want to add limits on the velocity to not move too fast. This can be done with an if-statement where you check that the value is not too big before adding more to it, or too smal before subtracting from it. Good luck!
I have a working code which basically paints 15 rectangles on the screen that you can drag around. I made it so that the rectangles falls to the bottom of the screen as time passes. While I have the thread.sleep method at bigger numbers such as 500, I can still drag the rectangles around the screen as they fall with no problems. But as I start to decrease the thread.sleep method to smaller numbers such as 50, suddenly problems arises. Problems such as I can only drag up to 2 rectangles before the rectangles start glitching back to the places where I did not drag them. Sometimes I can only drag up to one rectangles, and once I selected that rectangle, I can't select any other rectangles to drag. I know my codes are definitely right, since it works while the thread.sleep method is at at bigger number, so my question is: why does it start glitching when I make thread.sleep to smaller numbers? Here's part of my code.
while (true) {
for (int i = 0; i < 15; i++) {
P.fY[i]++;
}
Thread.sleep(500);
frame.repaint();
} //the 15 stands for 15 rectangles, and the P.fY stands for the position of y.
So based off of your comment, it seems like you just really need a hand with figuring out how to calculate the distance as a function of time.
By adding 1 each frame loop, you're really saying the speed of each square is 1 pixel / 1 frame.
Instead, you should utilize time and update the distance by a function of time, so that it will be 1 pixel / unit of time. This means the velocity of the squares will then be independent of the frames per second.
I whipped up a code example. The important method is the Square#doUpdate() method. This pertains to exactly what you're looking for.
The procedure it follows is:
Calculate time from last update, store it in delta.
Update the time of the last update to the current time
Calculate deltaX, which is deltaX = delta * velocityX
Calculate deltaY, which is deltaY = delta * velocityY
Add deltaX to x - this updates the x coordinate
Add deltaY to y - this updates the y coordinate
The code is as follows:
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.WindowConstants;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.LinkedList;
/**
* #author Obicere
*/
public class MovingSquare {
private volatile int viewportWidth;
private volatile int viewportHeight;
private final LinkedList<Square> squares = new LinkedList<>();
public MovingSquare() {
final JFrame frame = new JFrame("Moving Square");
final JPanel displayPanel = new JPanel() {
#Override
protected void paintComponent(final Graphics g) {
synchronized (squares) {
for (final Square square : squares) {
// Update the square's locations, ideally this will
// be separate of the painting thread
square.doUpdate();
final int x = (int) square.getX();
final int y = (int) square.getY();
g.setColor(square.getColor());
g.drawRect(x, y, square.squareSize, square.squareSize);
}
}
}
};
displayPanel.addMouseListener(new MouseAdapter() {
#Override
public void mouseReleased(final MouseEvent e) {
final Color nextColor = Color.getHSBColor((float) Math.random(), 1, 0.5f);
final float speedX = (float) Math.random();
final float speedY = (float) Math.random();
synchronized (squares) {
final Square newSquare = new Square(nextColor, speedX, speedY);
squares.add(newSquare);
newSquare.x = e.getX();
newSquare.y = e.getY();
}
}
});
displayPanel.addComponentListener(new ComponentAdapter() {
#Override
public void componentResized(ComponentEvent e) {
viewportWidth = displayPanel.getWidth();
viewportHeight = displayPanel.getHeight();
}
});
final Timer repaintTimer = new Timer(20, null);
repaintTimer.addActionListener(e -> {
if (!frame.isVisible()) {
repaintTimer.stop();
return;
}
frame.repaint();
});
repaintTimer.start();
displayPanel.setPreferredSize(new Dimension(200, 200)); // Sorry MadProgrammer
frame.add(displayPanel);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(MovingSquare::new);
}
private class Square {
private final int squareSize = 25;
private volatile float x;
private volatile float y;
private volatile long lastUpdateTime;
private volatile boolean negateX;
private volatile boolean negateY;
private final float speedX;
private final float speedY;
private final Color color;
public Square(final Color color, final float speedX, final float speedY) {
this.color = color;
this.speedX = speedX;
this.speedY = speedY;
lastUpdateTime = System.currentTimeMillis();
}
/**
* Important method here!!
* <p>
* This updates the location of the squares based off of a set
* velocity and the difference in times between updates.
*/
public void doUpdate() {
// Gets the change in time from last update
final long currentTime = System.currentTimeMillis();
final long delta = currentTime - lastUpdateTime;
if (delta == 0) {
return;
}
// be sure to update the last time it was updated
lastUpdateTime = currentTime;
// Calculate the speed based off of the change in time
final float deltaX = getSpeedX(delta);
final float deltaY = getSpeedY(delta);
// Move each square by the change of distance, calculated from
// the change in time and the velocity.
final float nextX = x + deltaX;
final float nextY = y + deltaY;
handleBouncing(nextX, nextY);
}
private void handleBouncing(final float nextX, final float nextY) {
if (nextX < 0) {
x = 0;
flipX();
} else if (nextX + squareSize >= viewportWidth) {
x = viewportWidth - squareSize;
flipX();
} else {
x = nextX;
}
if (nextY < 0) {
y = 0;
flipY();
} else if (nextY + squareSize >= viewportHeight) {
y = viewportHeight - squareSize;
flipY();
} else {
y = nextY;
}
}
private float getSpeedX(final long delta) {
return (negateX ? -1 : 1) * delta * speedX;
}
private float getSpeedY(final long delta) {
return (negateY ? -1 : 1) * delta * speedY;
}
protected void flipX() {
negateX = !negateX;
}
protected void flipY() {
negateY = !negateY;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public Color getColor() {
return color;
}
}
}
And it in action:
This might seem a bit overwhelming. Step through it, change some things up. Go crazy and see what the results are.
There are also some websites that can help with velocity and how to calculate things like this. If you need further help, just drop a comment down below and I'll see what I can do.