I'm still pretty confused as to when / how to end threads in my Surfaceview application and was hoping someone could explain.
Currently, I am using this code:
Log.v("Destroyed","Surface Destroyed");
preThread.setRunning(false);
boolean retry = true;
while (retry) {
try {
preThread.join();
retry = false;
} catch (InterruptedException e) {
}
}
The above code sits in my surfaceDestroyed method - firstly, is this correct?
In my surfaceCreated method I have the following code which should check to see if the thread still exists or has been stopped and if it's been stopped, then re-start it:
if (runthread==false){
if (preThread.getState()==Thread.State.TERMINATED){
preThread = new MainThread(thisholder, thiscontext, thishandler);}
else {}
preThread.setRunning(true);
preThread.start();
}
It seems to act really strange. Here is what I get:
*) When I first install the game and run it, through my logging, it says that the thread already exists, If I then press the back key, surfaceDestroyed is run but when I go back into the activity, again it says that the thread already exists.
*) If I press the home key, then surfaceDestroyed is run and when I go back into the activity it says that the thread was previously destroyed and starts a new one.
*) If I kill the activity using DDMS, surfaceDestroyed isn't run and when I go back into the activity it says that the thread already exists.
If I'm thinking straight, then the third scenario is the only one that seems to make sense.
I'm clearly doing something drastically wrong. The main problem is this:
If I hit the home key during the game and then end the app via DDMS in Eclipse, restart the app and hit the back key twice in quick succession (once, to go back to the previous activity, then again to get back to the splash screen) - the app force-closes and I get a "Fatal exception: Thread 12" in logcat. I have to assume this is because my thread is never ending and is trying to be-restarted? I'm not sure.
I've been trying to figure this out for what seems like an age so I really hope someone can explain what I'm doing wrong!
Many thanks!
Edit. Logcat output.
my Run() method:
public void run(){
//Main Loop
while (runthread){
Log.v("tracking","runthread is: "+runthread); //This should only be logged while this loop is running
timestart = System.currentTimeMillis(); //Get time at start of loop for FPS calc
try{c=mySurfaceHolder.lockCanvas(); //Set Canvas to locked
synchronized(mySurfaceHolder){
if (c==null){Log.v("Stop","Canvas is null for some reason - exiting, "+c+" - see?!!!");}
framesskipped = 0; // resetting frames skipped
doDraw(c); //Draw to the screen
updateMenu();
}
}
finally{
if (c != null){
mySurfaceHolder.unlockCanvasAndPost(c); //Post canvas
}
}
//work out timings
timeend = System.currentTimeMillis(); //get end time for current frame (for FPS)
frametime = timeend-timestart; //Set the frametime variable to the time the frame took to render & update (end time - start time)
sleepfor = (int) (33-frametime); // this is the time that the thread will sleep for if <target time
if (sleepfor>0){ // If the 'sleepfor' variable is >0 then set the thread to sleep for it's value (expressed in ms)
try {
OptionsThread.sleep(sleepfor); //send thread to sleep for value of sleepfor (determined above).
} catch (InterruptedException e) {} //in case of exception
} //close if statement
while (sleepfor<0 && framesskipped<maxframesskipped){ //if sleepfor is < 0 (ie, frame took longer to render than target time and the maxframesskipped has not reached it's limit)
updateMenu(); //Update animation variables without rendering to the screen while these conditions are met
sleepfor+=33; //time to sleep plus the time frame took to render
framesskipped++; //add one to framesskipped variable so this only skips a certain number of frames
}
}
}
New Logcat output showing nullPointerException and output of logging. runThread is never logged as false so I'm not sure how the line that logs canvas as null is reached!
Thanks
Edit:
OK, I've completely started from scratch and re-written the whole class- it's very much a stripped-down version of what I had before and here is the whole class:
import android.content.res.Resources;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class OptionsScreen extends SurfaceView implements
SurfaceHolder.Callback {
//Create Variables
private SurfaceHolder thisHolder;
private Context thisContext;
private Handler thisHandler;
private preThread thread;
private Bitmap background;
private Resources res;
private Context myContext;
private Handler myHandler;
private Canvas c;
// thisholder = getHolder();
public OptionsScreen(Context context) {
super(context);
myContext=context; //This is the context passed into this constructor (this)
thisHolder = getHolder(); //Get surface holder
thisHandler=getHandler(); //Get Handler
thisContext = getContext(); //Get context
res=getResources(); //Get resource
//add the callback surface holder
getHolder().addCallback(this);
//make focusable
setFocusable(true);
//create new thread
thread = new preThread(thisHolder, thisContext, thisHandler);
//create bitmaps from resources
background = BitmapFactory.decodeResource(res, R.drawable.sky);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.v("check","surfaceChanged run");
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("check","surfaceCreated run"+thread.getState());
int height = this.getHeight();
int width = this.getWidth();
if(thread.getState()==Thread.State.TERMINATED){ //Has thread been stopped previously? could happen if the home key is pressed
Log.v("check","Thread still exists!!!! - Starting a new one. "+thread.getState());
thread = new preThread(thisHolder, thisContext, thisHandler);
}
thread.setRunning(true);
thread.start();
Log.v("check","Thread - "+thread.getState());
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("check","surfaceDestroyed run"+thread.getState());
thread.setRunning(false); //Set to false to exit run() method
boolean retry = true; //Shut off rendering thread
while (retry) {
try {
thread.join();
retry = false;
} catch (InterruptedException e) {
// try again shutting down the thread
}
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("check","Surface Touched");
Log.v("check","Thread - "+thread.getState());
// System.exit(0);
return super.onTouchEvent(event);
}
#Override
protected void onDraw(Canvas canvas) {
// if (canvas!=null){
canvas.drawBitmap(background, 0, 0, null);
Log.v("Stop","Canvas is "+canvas);
}
}
//*******************************************************************
//** run loop **
//*******************************************************************
protected class preThread extends Thread {
private SurfaceHolder mySurfaceHolder;
private Context myContext;
public preThread(SurfaceHolder surfaceholder, Context context, Handler handler) { //Constructor
mySurfaceHolder=surfaceholder;
myContext=context;
res = myContext.getResources();
}
// flag
private boolean running;
public void setRunning(boolean running) {
this.running = running;
}
#Override
public void run() {
while (running) {
try{c=mySurfaceHolder.lockCanvas();
synchronized(mySurfaceHolder){
Log.v("check","Drawing!!");
onDraw(c);
}
}
finally{
if (c != null){
mySurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
}
Threads are pretty difficult to manage, but after some hit and trial i think i have come up with a scheme which works correctly most of the time.
To End a Thread
if(m_hThread != null)
{
try
{
m_bThread = false; // m_bThread is the while condition of the thread
m_hThread.interrupt(); // incase the thread is in sleep
m_Thread.join(); // This call blocks and waits for thread to end
m_hThread = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
For re-creating thread
if(m_hThread == null)
{
m_bThread = true; //while condition of thread
StartNewThread();
}
In your implementation, No need to retry Thread.join, either it will join in the first try or it will block until the thread joins. As for your cases only the first case seems to be weird, where you find the thread is already running, this can't possibly be true. Second and third make complete sense to me, and work like they should. When the user clicks Home button surfaceDestroyed is invoked and thread is Terminated.
thread continues as long as m_bThread is true,
while(m_bThread) //b
{
// Continuous Thread operations...
}
m_hThread is simply preThread in your code, and m_Thread is a also m_hThread just a typing mistake here.
Related
I'm in the process of writing my first android app, an asteroids clone, and I've got one last bug I can't figure out. The game activity will occasionally freeze when the game ends and tries to go to the game over activity. The game activity has a few UI views that stop working before it freezes. There is a Button to pause the game which stops responding to input. There is also a TextView for the score that I am updating using runOnUIThread() (since I am running the game on a separate thread) which stops updating.
There are a few things that make this puzzling to me. First off, I'm not seeing any error messages. The bug only happens about 1 out of every 5 times the game activity is started and it happens at random points while it is running. It happens when the game activity starts about half the time, but seemingly may occur at any point, although it does tend to happen sooner than later even when it doesn't occur right at the start. Also, the game thread and the custom game View I made keep updating fine when the bug occurs. Finally, I have two virtual joystick Views I made to control the player's motion and shooting which also keep updating and responding to touch input fine. These last two really confuse me because at first I thought the main thread was doing too much work or getting stuck in a loop or something, but if I understand correctly (and according to some tests I've done) drawing and touch input happen on the main thread, and as far as I can tell all views are drawing fine and touch input is working for the joysticks but not the pause button. Also, if the main thread was unresponsive, wouldn't I get an app not responsive popup? That never happens though.
I've tried removing all instances of runOnUIThread() just to see if that fixed it with no effect other than breaking the score TextView and leaving the app stuck in the game activity even when the bug doesn't occur. I've tried printing the names of the threads returned by Thread.currentThread() in the game loop, in the onDraw() method of the game view, in the onTouch() method of JoystickView and when updating the score TextView in the game activity to ensure I actually am running 2 separate threads and everything is running on the threads they should be (game thread for the game loop, main thread for everything else), and that checked out. I've tried cutting down on creating new objects during the game loop, not sure what that would do other than help performance to be honest but I figured it couldn't hurt, also to no effect.
This all makes it difficult for me to know what code to post. Here are some guesses:
The onCreate, goToGameOverActivity, and updateScoreTextView methods from my game activity:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
setContentView(R.layout.activity_play);
gameView = findViewById(R.id.gameView);
game = new Game(this, gameView);
float joystickRadius = 150;
CartesianPoint2D motionJoystickCenter = new CartesianPoint2D(
10 + joystickRadius,
Constants.getScreenInformation().getHeight() - 10 - joystickRadius);
motionJoyStickView = new MotionJoystickView(this, joystickRadius, motionJoystickCenter, game);
CartesianPoint2D shootJoystickCenter = new CartesianPoint2D(
Constants.getScreenInformation().getWidth() - 10 - joystickRadius,
Constants.getScreenInformation().getHeight() - 10 - joystickRadius);
shootJoystickView = new ShootJoystickView(this, joystickRadius, shootJoystickCenter, game);
scoreTextView = findViewById(R.id.playActivityScoreTextView);
pauseButton = findViewById(R.id.playActivityPauseButton);
soundEffects = new SoundEffects(this);
game.start();
}
public void goToGameOverActivity() {
game.stop();
Intent intent = new Intent(this, GameOverActivity.class);
startActivity(intent);
finish();
}
public void updateScoreTextView(int score) {
scoreTextView.setText(String.valueOf(score));
scoreTextView.invalidate();
}
The Runnables I am using to update the score TextView and signal that the game is over using runOnUIThread():
package com.group18.spacerocks.play.view;
import com.group18.spacerocks.play.PlayActivity;
public class UpdateScoreTextView implements Runnable {
protected int newScore;
protected PlayActivity playActivity;
public UpdateScoreTextView(int newScore, PlayActivity playActivity) {
System.out.println("Setting ScoreTextView to " + newScore + ".");
this.newScore = newScore;
this.playActivity = playActivity;
}
#Override
public void run() {
playActivity.updateScoreTextView(newScore);
}
}
package com.group18.spacerocks.play;
public class GoToGameOverActivity implements Runnable {
protected PlayActivity playActivity;
public GoToGameOverActivity(PlayActivity playActivity) {
this.playActivity = playActivity;
}
#Override
public void run() {
playActivity.goToGameOverActivity();
}
}
The run() method from the GameThread class extending thread which updates the game:
public void run() {
super.run();
while (running) {
while (paused) {
currentTime = System.nanoTime();
lastUpdateTime = currentTime;
try {
sleep(1);
}
catch (InterruptedException e) {
e.printStackTrace()
}
}
currentTime = System.nanoTime();
timeSinceLastUpdate = currentTime - lastUpdateTime;
if (timeSinceLastUpdate >= TARGET_UPDATE_LENGTH) {
currentUpdateLength = currentTime - lastUpdateTime;
game.update(currentUpdateLength);
gameView.invalidate();
lastUpdateTime = currentTime;
UPS++;
timeSinceLastUpdate = 0;
if (currentTime - lastSecondTime >= 1000000000) {
System.out.println("UPS: " + UPS);
UPS = 0;
lastSecondTime = currentTime;
}
}
}
}
And finally the code for my JoystickView class, which has 2 child classes for motion and shooting:
package com.group18.spacerocks.play.joystick;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.group18.spacerocks.R;
import com.group18.spacerocks.play.CartesianPoint2D;
import com.group18.spacerocks.play.PlayActivity;
import com.group18.spacerocks.play.Game;
public class JoystickView extends View implements View.OnTouchListener {
protected boolean inUse;
protected float radius;
protected float buttonRadius;
protected CartesianPoint2D center;
protected CartesianPoint2D buttonOffset;
protected PlayActivity playActivity;
protected Game game;
protected Paint outlinePaint;
protected Paint buttonPaint;
public JoystickView(PlayActivity playActivity, float radius, CartesianPoint2D center, Game game) {
super(playActivity);
inUse = false;
this.radius = radius;
buttonRadius = radius / 2;
this.center = center;
this.buttonOffset = CartesianPoint2D.getInstance();
this.playActivity = playActivity;
this.game = game;
setLayoutParams(new ViewGroup.LayoutParams((int) radius * 2, (int) radius * 2));
setX(center.getX() - radius);
setY(center.getY() - radius);
outlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
outlinePaint.setAlpha(100);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setColor(Color.WHITE);
outlinePaint.setStrokeWidth(1);
buttonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
buttonPaint.setAlpha(100);
buttonPaint.setStyle(Paint.Style.STROKE);
buttonPaint.setColor(playActivity.getResources().getColor(R.color.textTitleColor));
buttonPaint.setStrokeWidth(10);
setOnTouchListener(this);
ViewGroup viewGroup = playActivity.findViewById(R.id.playActivityLayout);
viewGroup.addView(this);
}
public void onDraw(Canvas canvas) {
canvas.drawCircle(radius, radius, radius, outlinePaint);
canvas.drawCircle(radius + buttonOffset.getX(), radius + buttonOffset.getY(), buttonRadius, buttonPaint);
}
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
inUse = false;
buttonOffset.setX(0);
buttonOffset.setY(0);
}
else {
inUse = true;
CartesianPoint2D motionPosition = new CartesianPoint2D(motionEvent.getX(), motionEvent.getY());
CartesianPoint2D centerOffset = new CartesianPoint2D(motionPosition.getX() - radius, motionPosition.getY() - radius);
if (centerOffset.getMagnitude() > radius) {
buttonOffset = CartesianPoint2D.createFromMagnitudeAndDirection(radius, centerOffset.getDirection());
} else {
buttonOffset = centerOffset;
}
}
invalidate();
return true;
}
}
Any insight is appreciated, I've been trying to fix this for a few days now and I'm at a loss.
Still don't know exactly what the cause was, but I fixed it by making the game view an extension of SurfaceView so I could draw in a thread other than the main thread.
I'm writing a simple Whack a Mole clone, and I've got my UI elements declared in a GridLayout in a layout.xml, then assigned to ImageView variables in an array programmatically. I've got a startGame() method that simply takes a random int, pulls it from the array and causes it to go visible for a second, then repeats. For some reason, when I put this code in a while() loop, it causes my UI to go blank as soon as it's launched.
I know it's the while() loop because I tried taking the code out of the while() loop, and it ran correctly (once), but turns everything white when placed in a while loop.
Here's the method causing the problem:
public void startGame() {
gameStarted = true;
while(gameStarted) {
randomInt = rand.nextInt(11);
mole[randomInt].setVisibility(View.VISIBLE);
handler.postDelayed(new Runnable() {
#Override
public void run() {
mole[randomInt].setVisibility(View.INVISIBLE);
}
}, 5000);
}
}
All the other relevant code is in onCreate, it's otherwise just a skeleton Activity subclass.
public class WAM_Activity extends Activity {
private ImageView[] mole = new ImageView[11];
private int[] moleId = {R.id.mole1, R.id.mole3, R.id.mole4, R.id.mole5, R.id.mole6, R.id.mole7, R.id.mole8, R.id.mole9, R.id.mole10, R.id.mole11, R.id.mole12};
private boolean gameStarted;
private int randomInt = 0;
private Random rand = new Random();
Handler handler = new Handler();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.wam_view_layout);
for (int i = 0; i < 11; i++) {
mole[i] = (ImageView) findViewById(moleId[i]);
mole[i].setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//do stuff eventually
}
});
}
gameStarted = true;
startGame();
}
Any idea why this isn't working? I've been staring at it for hours and I'm quite stumped.
Android doesn't work that way, when onCreate is called, it need to be finished in order for the app to keep responding, I'm surprised you are not getting any "App not respopnding" error.
If you want to create a "game loop" you can simply by creating a new Thread and putting the while in there.
Activity's lifecycle must be executed without blocking them for the app to operate correctly, for more info check here.
Do you know about threads? if you want i can post an example of how to do that with threads but it might be long and if you don't know what a Thread is it will be too confusing for you.
Edit: Ok I'll make an example of a Thread
When I create my games I usually have only one Activity that the only thing it does is creating a custom SurfaceView and nothing else.
public class GameActivity extends Activity
{
//This is a custom class that extends SurfaceView - I will write it below
private GameSurface game;
#Override
protected void onCreate(Bundle bundle)
{
super.onCreate(bundle);
//Create a new instance of the game
game = new GameSurface(this);
//Set the View of the game to the activity
setContentView(game);
}
}
You can also add extra stuff like onSaveInstanceState to save game data and restore them later but I don't want to add them now so the code looks simple.
This class was very simple, let's move on to our SurfaceView. The reason I picked a SurfaceView to do that it's because it is made to allow custom graphics to be drawn on it - exactly what we want on a video game. I will try to make the class as simple as possible:
/*SurfaceHolder.Callback will run some functions in our class when
our surface is completed - at that point we can initialize data
that have to do with the View's width/height.
I don't know if you've noticed that on a View's onCreate()
when you call getWidth() or getHeight() you get 0, that's because
the surface is not initialized yet, this is a way to fix that.
Also we need a Runnable to run the Thread inside this class,
no need to make more classes and make it more complicated*/
public class GameSurface extends SurfaceView
implements SurfaceHolder.Callback, Runnable
{
//This is our thread - we need the "running" variable to be
//able to stop the Thread manually, this will go inside our "while" loop
private Thread thread;
private boolean running;
//Right here you can add more variables that draw graphics
//For example you can create a new class that has a function that
//takes Canvas as a parameter and draws stuff into it, I will add
//a Rect in this case which is a class already made by android
//but you can create your own class that draws images or more
//complicated stuff
private Rect myRect;
//Rect needs a paint to give it color
private Paint myPaint;
//Constructor
public GameSurface(Context context)
{
super(context);
//This is the callback to let us know when surface is completed
getHolder().addCallback(this);
}
//When a class implements SurfaceHolder.Callback you are forced to
//create three functions "surfaceCreated", "surfaceChanged" and
//"surfaceDestroyed" these are called when the surface is created,
//when some settings are changed (like the orientation) and when
//it is about to be destroyed
#Override
public void surfaceCreated(Surface holder)
{
//Let's initialize our Rect, lets assume we want it to have 40
//pixels height and fill the screen's width
myRect = new Rect(0, 0, getWidth(), 40);
//Give color to the rect
myPaint = new Paint();
myPaint.setARGB(0, 255, 0, 0);
//In case you are not familiar with the Rect class, as
//parameters it gets Rect(left, top, right, bottom)
//Time to start our Thread - nothing much to explain here if
//you know how threads work, remember this class implements
//Runnable so the Thread's constructor gets "this" as parameter
running = true;
thread = new Thread(this);
thread.start();
}
//We won't use this one for now, but we are forced to type it
//Even if we leave it empty
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
//When the surface is destroyed we just want the Thread to
//terminate - we don't want threads running when our app is not visible!
#Override
public void surfaceDestroyed(SurfaceHolder holder)
//We will type this function later
{destroyThread();}
//Time for the interesting stuff! let's start with input
#Override
public boolean onTouchEvent(MotionEvent event)
{
//The logic is as follows: when our Rect is touched, we want
//it to become smaller
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
if (myRect.contains((int) event.getX(), (int) event.getY())
{
myRect.right -= 5;
//Return true - we did something with the input
return true;
}
}
return super.onTouchEvent(event);
}
//This is our update, it will run once per frame
private void update()
{
//Let's assume we want our rect to move 1 pixel downwards
//on every frame
myRect.offset(0, 1);
}
//Now for our draw function
public void draw(Canvas canvas)
{
//Here we want to draw a background and our rect
canvas.drawARGB(0, 0, 0, 255);
canvas.drawRect(myRect, myPaint);
}
//The only thing left is our run() function for the Thread
#Override
public void run()
{
//Screen
Canvas canvas;
//Our game cycle (the famous while)
while(running)
{
//Count start time so we can calculate frames
int startTime = System.currentTimeMillis();
//Update our game
update();
//Empty screen so it can obtain new instance
canvas = null;
//Try locking the canvas for pixel editing on surface
try
{
//Try getting screen
canvas = getHolder().lockCanvas();
//Succeeded
if (canvas != null) synchronized (getHolder())
{
//Actual drawing - our draw function
draw(canvas);
}
} finally
{
//Draw changes
if (canvas != null) getHolder().unlockCanvasAndPost(canvas);
}
//End Frame - 1000/30 means 30 frames per second
int frameTime = System.currentTimeMillis() -startTime;
if (frameTime < 1000/30)
try { Thread.sleep(1000/30 -frameTime); } catch (InterruptedException e){}
}
}
//Last but not least, our function for closing the thread
private void destroyThread()
{
//Stop thread's loop
running = false;
//Try to join thread with UI thread
boolean retry = true;
while (retry)
{
try {thread.join(); retry = false;}
catch (InterruptedException e) {}
}
}
}
I may have made some minor mistakes (probably with case sensitive letters) so feel free to correct these, I wrote the code at once so I didn't have time to test it, it should work flawlessly though.
If you have any more questions, need more explanation or something is not working right let me know!
This is my first post here. Sorry if it is not well done and thanks in advance.
There is something that is driven me crazy.
I try to draw something and show it slowly on screen. For this I use Thread.sleep as shown in the code. But it is showing two different versions of the canvas in each iteration.
Can anybody explain what is wrong?
I only want to draw something and after a few seconds draw something else. And so on.
This is my code:
public class Vista extends SurfaceView implements Callback {
private Hilo hilo;
private SurfaceHolder sf;
class Hilo extends Thread {
public boolean running;
private Vista view;
Canvas c = null;
Bitmap btm = null;
public Hilo(SurfaceHolder holder, Vista view) {
sf = holder;
btm = Bitmap.createBitmap(480, 800, Config.ARGB_8888);
c = new Canvas(btm);
}
#Override
public void run() {
Paint paint = new Paint();
paint.setStyle(Style.FILL_AND_STROKE);
paint.setColor(Color.BLUE);
int k = 0;
while (running) {
c = sf.lockCanvas();
k+= 10;
c.drawText(String.valueOf(k/10), k, 20, paint);
sf.unlockCanvasAndPost(c);
try {
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public Vista(Context context) {
super(context);
SurfaceHolder holder = getHolder();
holder.addCallback(this);
hilo = new Hilo(holder, this);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// TODO Auto-generated method stub
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
hilo.running = true;
hilo.start();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
hilo.running = false;
while (retry) {
try {
hilo.join();
retry = false;
} catch (InterruptedException e) {
}
}
}
}
Your code is sleep()ing the Main thread, also known as the UI thread. This is something you absolutely mustn't do, as the main thread is responsible for handling all "stuff" in Android: UI rendering, UI event dispatch, etcetera. You can read the Processes and Threads guide on the Android Developer website for more information. Choice quote:
Thus, there are simply two rules to Android's single thread model:
Do not block the UI thread
Do not access the Android UI toolkit from outside the UI thread
The question then becomes, how do you animate a view without sleeping the main thread? The trick is to tell Android to go do something else and come back to you (redraw you) after a little while. Each time you draw, you calculate by how much to advance your animation.
One (crude) way to do this is with postInvalidateDelayed(long), which does exactly that: redraw your view after the given number of milliseconds. It's not a precise delay, it'll fluctuate a bit, so you can't assume that's exactly how much time passed. A way to do this is to store the System.nanoTime() in an instance field, so that the next time you're drawn, you can take the difference to a new reading of nanoTime(). This difference is how much time has passed and thus tells you by how much you should update your animation state. If your animation is supposed to last 1 second in total and 100 milliseconds went by since the last time you drew your scene, you must advance everything by 10%.
Android provides the a lot of utilities to make this easier.
After searching google, various android blogs, various game dev blogs, and other tutorial sites for surfaceviews in android I'm looking to get a complete understanding of surfaceviews. I've read several books on Safaribooks about android and surface views, but they provide either too little information, or use other SDKs such as AndEngine. I was hoping to learn strictly surface view. I've played with the Lunar Lander sample project as well as other projects I've found and have created some code of a skeleton surfaceview. It consists of 3 classes just for the skeleton.
The MainActivity class:
package com.learning.svlearning;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.Window;
import android.view.WindowManager;
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Set FullScreen Mode - No title bars!!
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
// Screen created with pure java - Say no to xml (atleast for this demo)
setContentView(new MainGamePanel(this));
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.layout_game_window, menu);
return true;
}
}
This class is pretty straight forward. The main game activity window, requesting full screen with no title bars. How a real game should be :) This class calls our next class for the view by passing "this" (MainActivity) class's context.
MainGamePanel class:
package com.learning.svlearning;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MainGamePanel extends SurfaceView {
final static public String tag = "Tracer";
private GameThread gameThread; // For our thread needed to do logical processing without holding up the UI thread
private SurfaceHolder holder; // For our CallBacks.. (One of the areas I don't understand!)
public MainGamePanel(Context context) {
super(context);
Log.d(tag, "Inside MainGamePanel");
gameThread = new GameThread(this); //Create the GameThread instance for our logical processing
holder = getHolder();
holder.addCallback(new SurfaceHolder.Callback() {
// Since we are using the SurfaceView, we need to use, at very least, the surfaceDestroyed and surfaceCreated methods.
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
Log.d(tag, "Inside SurfaceHolder Callback - surfaceDestroyed");
gameThread.setRunning(false); // Stop the Thread from running because the surface was destroyed. Can't play a game with no surface!!
while (retry) {
try {
Log.d(tag, "Inside SurfaceHolder Callback - surfaceDestroyed - while statement");
gameThread.join();
retry = false; //Loop until game thread is done, making sure the thread is taken care of.
} catch (InterruptedException e) {
// In case of catastrophic failure catch error!!!
}
}
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
// let there be Surface!
Log.d(tag, "Inside SurfaceHolder Callback - surfaceCreated");
gameThread.setRunning(true); // Now we start the thread
gameThread.start(); // and begin our game's logical processing
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
// The code to resize the screen ratio when it flips from landscape to portrait and vice versa
}
});
}
#Override
protected void onDraw(Canvas canvas) {
//This is where we draw stuff.. since this is just a skeleton demo, we only draw the color Dark Grey so we can visibly see that we actually accomplished something with the surfaceview drawing
Log.d(tag, "Inside onDraw");
canvas.drawColor(Color.DKGRAY); // You can change the Color to whatever color you want, for this demo I just used Color.DKGRAY
}
}
This class mainly deals with the drawing of our resources/images with the onDraw method, handling what happens when our surface is created and destroyed (also when the screen changes, but i didnt write any code to handle it for now), and calls our GameThread class which handles the processing of our game logic.
GameThread class:
package com.learning.svlearning;
import android.graphics.Canvas;
import android.util.Log;
public class GameThread extends Thread{
final static public String tag = "Tracer";
private MainGamePanel view;
private boolean running = false;
static final long FPS = 30; // To help limit the FPS when we draw, otherwise we would kill the CPU and increase the Battery Consumption.
public GameThread(MainGamePanel view){
Log.d(tag, "inside GameThread");
this.view = view;
}
public void setRunning(boolean run){
Log.d(tag, "inside GameThread - setRunning");
running = run; // For starting / stoping our game thread
}
#Override
public void run() {
long ticksPS = 1000 / FPS; // Limit the frames per second
long startTime;
long sleepTime;
Log.d(tag, "inside GameThread - run");
while(running){ // Our Main Game Loop is right here
Canvas c = null; // build our canvas to draw on
Log.d(tag, "inside GameThread - run - while loop");
startTime = System.currentTimeMillis(); //get the current time in milliseconds - this is for helping us limit the FPS
try{
c = view.getHolder().lockCanvas(); //Before we can draw, we always have to lock the canvas, otherwise goblins will invade your app and destroy everything!
synchronized (view.getHolder()){ // we have to synchronize this because we need to make sure that the method runs when at the proper time.
view.onDraw(c); // this is where we pass our drawing information. The canvas gets passed to the onDraw method in our MainGamePanel class.
}
}finally{
if(c != null) {
view.getHolder().unlockCanvasAndPost(c); // Once we are done drawing, we unlock our canvas and post. which means we drew on the canvas, and now the devices screen will display our drawing.
}
}
sleepTime = ticksPS-(System.currentTimeMillis() - startTime); // this is where we calculace how long we need this thread to sleep (again with the FPS) we want it limited to 30 FPS as defined in our FPS variable.
try {
if (sleepTime > 0){
sleep(sleepTime); // night night, sleep to limit the fps and save our batteries!
}
else{
sleep(10); // Incase something goes crazy, we still want to sleep the thread to save the battery.
}
}catch(Exception e){
}
}
}
}
This class deals with processing game logic and sending any drawable information to our draw method in the MainGamePanel class. Such as if we had a character moving, we could send the x and y coordinate to our draw method to draw our character in a different position.
Now some parts of these classes I don't really understand fully. Like the callback, When reading the information on the googles android developers page, i'm just left more confused on what it is and why we use it. Also, if anyone has anything more to put into this, or sees anything I may have mis-understood, feel free to correct me. I enjoy working with android, though it is pretty difficult, when you start to figure things out, it is very rewarding!
The purpose of the SurfaceHolder callback is to handle those 3 events. surfaceCreated(), surfaceDestroyed(), and surfaceChanged().
If you read the code you can see that surfaceCreated() handles things that need to happen when the surface is created and so on...
Was that the answer to your question or was there more?
You should change your drawing method to be named something other than onDraw() when using a SurfaceView. That way the main thread can never accidentally invalidate() it!
To use a SurfaceView for drawing a 2D game in Android, I use this in the main activity's onCreate():
setContentView(new GameView(this));
Which is a reference to this class:
public class GameView extends SurfaceView implements SurfaceHolder.Callback
Additionally, I have a thread with its run() function:
public void run() {
Canvas c;
while (_run) {
c = null;
try {
c = _surfaceHolder.lockCanvas(null);
synchronized (_surfaceHolder) {
_panel.updatePhysics();
_panel.onDraw(c);
}
}
finally { // when exception is thrown above we may not leave the surface in an inconsistent state
if (c != null) {
_surfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
In updatePhysics() I do some calculations. They are more complex than this simple example, of course, but work the same way:
public void updatePhysics() {
GraphicObject.Coordinates coord;
GraphicObject.Speed speed;
for (GraphicObject graphic : _allElements) {
coord = graphic.getCoordinates();
speed = graphic.getSpeed();
coord.setX(coord.getX() + speed.getX());
coord.setY(coord.getY() + speed.getY());
...
}
}
And in onDraw(), I draw everything to the canvas:
#Override
public void onDraw(Canvas canvas) {
canvas.drawBitmap(BITMAP, xPos, yPos, null);
...
}
This works fine - everything. And when I tested it on my device, it looked pretty good. But when I gave it to someone else and he did a test game, the objects were moving much faster! Why is this so? Because the thread calls updatePhysics() as often as possible which means that fast devices call this function more often?
How can I prevent this and make the game equally fast on all devices? Something like this?
private long lastRun = System.currentTimeMillis();
public void updatePhysics() {
long millisPassed = System.currentTimeMillis()-lastRun;
...
float newCoord = (coord.getX() + speed.getX()) * millisPassed / 33;
coord.setX(newCoord);
...
}
Thanks for your help!
If you can, use the time directly to calculate all your physics. That would usually work best.
If you have no way to calculate based on time because what you are doing that is just step based and you know that generating the next step does not take much time then you have another option.
You create two threads. The first one advances the state at a fixed rate (and you have to be sure that this works on slow devices at that rate too). The second one takes the current state is sees and draws that. Now the second thread can be as slow as it wants because it simply skips some states (or draws the same state several times if it is faster).
Small example below has one thread that advances some state object and replaces the reference each time so the consuming thread does not need to worry that it's state object gets modified
class GameState {
private int state = 0;
public GameState advanceState() {
GameState result = new GameState();
result.state = this.state + 1;
return result;
}
}
class SurfaceViewImplementation extends SurfaceView {
// the current state
volatile GameState mState = new GameState();
void somewhere() {
Thread fastProducer = new Thread(new Runnable() {
private static final long MAX_WAIT = 1000 / 60;
#Override
public void run() {
while (!Thread.interrupted()) {
long timeBefore = SystemClock.currentThreadTimeMillis();
GameState newState = mState.advanceState();
mState = newState;
long timeAfter = SystemClock.currentThreadTimeMillis();
long timeSpent = timeAfter - timeBefore;
SystemClock.sleep(Math.max(0, MAX_WAIT - timeSpent));
}
}
});
fastProducer.start();
Thread slowConsumer = new Thread(new Runnable() {
#Override
public void run() {
while (!Thread.interrupted()) {
GameState currentState = mState;
longRunningDraw(currentState);
}
}
});
slowConsumer.start();
}
}
That will still fail to give you a speed independant result if the producing thread can't run at the desired rate.
I would save the time when I start rendering the frame(in your case is updatePhysics()), and then next time I would got to this point, I know how much time pass, if it's to fast you can use Thread.Sleep();