Статьи

Android SDK: Достижение движения

В этом уроке мы познакомимся с кратким руководством о том, как добиться движения объекта, одной из основ разработки игры. Мы не будем делать ничего фантастического, просто прыгаем мяч по экрану. Тем не менее, принципы в этом руководстве могут быть легко применены во многих более сложных сценариях. Без дальнейших проволочек, давайте сразу начнем!

Прежде чем мы начнем писать код, давайте создадим нашу базовую файловую структуру.

Сначала создайте свой проект Android. Назовите проект «Движение» и сопоставьте остальные настройки вашего проекта с фотографией ниже:

Вы заметите, что уровень API довольно низок, поскольку все функции, используемые здесь, являются очень простыми и используются с первого дня в Android.

Далее мы создадим еще два класса для нашего проекта. В частности, класс UpdateThread класс MovementView , которые являются нашими объектами Thread и SurfaceView соответственно. Подробнее об этом позже. Обратите внимание на имя класса и суперкласс этих классов; Я буду ссылаться на них на протяжении всего урока.

Это все, что нам нужно сделать для проекта! Если все прошло нормально, папка вашего проекта должна выглядеть так:

Не беспокойтесь об единственной ошибке, которая у вас есть в данный момент, она будет исправлена ​​очень скоро!

Прежде чем идти дальше, будьте осторожны: все эти фрагменты кода выглядят очень долго в этом формате! Но не волнуйтесь, в основном это просто комментарии к этому уроку. Без комментариев было бы меньше половины длины. Не позволяйте этому оттолкнуть вас!

Ваше приложение Activity, Movement.java, является основой вашего приложения. Он вызывается в самом начале жизненного цикла приложения и необходим для запуска в действие любой другой Activity или SurfaceView, которую вы, возможно, захотите запустить.

Наша деятельность будет очень простой. На самом деле, мы будем редактировать только одну строку кода! Это все, что нам нужно для запуска нашего класса MovementView .

1
2
3
4
package example.movement;
 
import android.app.Activity;
import android.os.Bundle;

Это первый код, выполненный в нашем приложении. Обратите внимание на использование метода setContentView (), где мы устанавливаем представление приложения в наш класс MovementView, в котором будет происходить все рисование. Мы также передаем «это» в конструктор.

1
2
3
4
5
6
7
8
9
public class Movement extends Activity {
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
 
        super.onCreate(savedInstanceState);
        setContentView(new MovementView(this));
    }
}

И это все, что нам нужно сделать с классом Activity для остальной части проекта!

Что такое SurfaceView? Наш SurfaceView — это класс MovementView.java. Все, что делает SurfaceView — это предоставляет большой планшет для нашего приложения. В этом представлении мы можем нарисовать холст, и он заменит его для нас. Мы будем использовать это, чтобы создать шар из чистого кода и рисовать его в новой позиции каждый раз, когда UpdateThread сообщает об этом. Давайте сразу же исправим эту досадную ошибку, созданную при запуске нашего проекта, открыв MovementView.java.

1
2
3
4
5
6
7
8
9
package example.movement;
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

Обратите внимание, что наш класс расширяет SurfaceHolder.Callback. Это то, что позволяет представлению обнаруживать такие вещи, как создаваемая, изменяемая или разрушаемая поверхность — очень полезно! Все последующие шаги будут методами и функциями, которые должны быть размещены внутри этого класса. Мы также определим наши переменные здесь, надеюсь, их имена станут понятны по мере продвижения учебника.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class MovementView extends SurfaceView implements SurfaceHolder.Callback {
    private int xPos;
    private int yPos;
 
    private int xVel;
    private int yVel;
 
    private int width;
    private int height;
 
    private int circleRadius;
    private Paint circlePaint;
 
    UpdateThread updateThread;
}

В этом методе мы решаем следующие задачи:

  • Вызовите метод super (), чтобы дать нам наш SurfaceView для работы.
  • Свяжите класс с SurfaceHolder.Callback
  • Инициализируйте переменные относительно круга
  • Установите скорость движения в каждом направлении
01
02
03
04
05
06
07
08
09
10
11
public MovementView(Context context) {
    super(context);
    getHolder().addCallback(this);
 
    circleRadius = 10;
    circlePaint = new Paint();
    circlePaint.setColor(Color.BLUE);
 
    xVel = 2;
    yVel = 2;
}

Это функция, которая будет вызываться для рисования круга на каждом кадре. Он выполняет две очень простые задачи:

  • Перекрасьте холст полностью в черный цвет, чтобы закрыть предыдущий кадр.
  • Нарисуйте новый круг в новом наборе координат.
1
2
3
4
5
6
7
8
@Override
protected void onDraw(Canvas canvas) {
 
    // you can always experiment by removing this line if you wish!
    canvas.drawColor(Color.WHITE);
 
    canvas.drawCircle(xPos, yPos, circleRadius, circlePaint);
}

Эта функция также называется каждым кадром и выполняет две задачи:

  • справиться с простой физикой движения
  • Обновите положение мяча и сделайте его «подпрыгивающим», если он достиг края

Что может быть запутанным в этом шаге, так это то, как приложение определяет, каким образом отскочить мяч. Основная концепция заключается в том, что если мяч касается боковой стороны, он переворачивается в направлении X. Если мяч попадает в верх или низ, он переворачивается в направлении Y. Кроме того, важно, чтобы при обнаружении столкновения мы немедленно установили мяч так, чтобы он лежал именно на этой стене, а не за ней, как это уже могло бы быть. Это предотвращает застревание мяча в петле за стеной.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public void updatePhysics() {
 
    xPos += xVel;
    yPos += yVel;
 
    if (yPos — circleRadius < 0 || yPos + circleRadius > height) {
 
        // the ball has hit the top or the bottom of the canvas
 
        if (yPos — circleRadius < 0) {
 
            // the ball has hit the top of the canvas
 
            yPos = circleRadius;
        }else{
 
            // the ball has hit the bottom of the canvas
 
            yPos = height — circleRadius;
        }
 
        // reverse the y direction of the ball
        yVel *= -1;
    }
    if (xPos — circleRadius < 0 || xPos + circleRadius > width) {
 
        // the ball has hit the sides of the canvas
 
        if (xPos — circleRadius < 0) {
 
            // the ball has hit the left of the canvas
 
            xPos = circleRadius;
        } else {
 
            // the ball has hit the right of the canvas
 
            xPos = width — circleRadius;
        }
 
        // reverse the x direction of the ball
        xVel *= -1;
    }
}

Этот метод вызывается при первом создании рабочей поверхности приложения. Вы должны заметить, что никогда не рекомендуется использовать размер экрана. Вместо этого используйте все необходимые средства для определения информации о ширине / высоте во время выполнения приложения. Этот метод решает следующие задачи:

  • Возьмите ограничивающую прямоугольник область холста и передайте ширину и высоту соответствующим переменным.
  • Установите начальную позицию мяча в верхней центральной части экрана.
  • Создайте UpdateThread и запустите его.

С момента запуска UpdateThread, шар начнет двигаться:

01
02
03
04
05
06
07
08
09
10
11
12
13
public void surfaceCreated(SurfaceHolder holder) {
 
    Rect surfaceFrame = holder.getSurfaceFrame();
    width = surfaceFrame.width();
    height = surfaceFrame.height();
 
    xPos = width / 2;
    yPos = circleRadius;
 
    updateThread = new UpdateThread(this);
    updateThread.setRunning(true);
    updateThread.start();
}

Этот метод не используется в нашем приложении, но требуется реализацией SurfaceHolder.Callback.

1
2
3
4
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
 
}

Этот метод имеет дело с выходом из приложения и закрытием потока. Если бы мы попытались закрыть приложение, не обращаясь к потоку, пользователь будет видеть очень раздражающее диалоговое окно с ошибкой каждый раз, когда он пытается закрыть приложение. Этот небольшой фрагмент кода позволит потоку завершить текущую задачу перед закрытием, что устранит ошибку.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public void surfaceDestroyed(SurfaceHolder holder) {
 
    boolean retry = true;
 
    updateThread.setRunning(false);
    while (retry) {
        try {
            updateThread.join();
            retry = false;
        } catch (InterruptedException e) {
 
        }
    }
}

Ваш окончательный файл MovementView теперь должен выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package example.movement;
 
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
public class MovementView extends SurfaceView implements SurfaceHolder.Callback {
 
    private int xPos;
    private int yPos;
 
    private int xVel;
    private int yVel;
 
    private int width;
    private int height;
 
    private int circleRadius;
    private Paint circlePaint;
 
    UpdateThread updateThread;
 
    public MovementView(Context context) {
 
        super(context);
        getHolder().addCallback(this);
 
        circleRadius = 10;
        circlePaint = new Paint();
        circlePaint.setColor(Color.BLUE);
 
        xVel = 2;
        yVel = 2;
    }
    @Override
    protected void onDraw(Canvas canvas) {
 
        canvas.drawColor(Color.WHITE);
        canvas.drawCircle(xPos, yPos, circleRadius, circlePaint);
    }
 
    public void updatePhysics() {
        xPos += xVel;
        yPos += yVel;
 
        if (yPos — circleRadius < 0 || yPos + circleRadius > height) {
            if (yPos — circleRadius < 0) {
                yPos = circleRadius;
            }else{
                yPos = height — circleRadius;
            }
            yVel *= -1;
        }
        if (xPos — circleRadius < 0 || xPos + circleRadius > width) {
            if (xPos — circleRadius < 0) {
                xPos = circleRadius;
            } else {
                xPos = width — circleRadius;
            }
            xVel *= -1;
        }
    }
 
    public void surfaceCreated(SurfaceHolder holder) {
 
        Rect surfaceFrame = holder.getSurfaceFrame();
        width = surfaceFrame.width();
        height = surfaceFrame.height();
 
        xPos = width / 2;
        yPos = circleRadius;
 
        updateThread = new UpdateThread(this);
        updateThread.setRunning(true);
        updateThread.start();
    }
 
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }
 
    public void surfaceDestroyed(SurfaceHolder holder) {
 
        boolean retry = true;
 
        updateThread.setRunning(false);
        while (retry) {
            try {
                updateThread.join();
                retry = false;
            } catch (InterruptedException e) {
            }
        }
    }
}

Если вы начинали как Flash Developer с ActionScript, как я, вы, вероятно, не будете знакомы с концепцией потоков. У Actionscript был фантастический способ справиться с движением, но в реальном мире дело обстоит иначе. Потоки можно использовать как эквивалент Android для OnFrameLoops, за исключением того, что они не так просты в использовании. С сайта разработчика Google:

Поток — это параллельная единица выполнения.

На простом английском языке это в основном означает, что потоки позволяют одновременно выполнять несколько задач в вашем коде.

Например: вы никогда не хотите, чтобы ваш основной класс Android ждал X времени, чтобы выполнить обновление. Это может привести к тому, что оно перестает отвечать, и пользователь, несомненно, расстроится. Вместо этого вы передаете ожидание другому парню — вашей пользовательской ветке. Он счастливо убегает на заднем плане и не прочь ждать Х времени. Ваш пользователь тоже счастлив, потому что интерфейс не зависит от того, когда поток завершает свою работу.

Давайте посмотрим на код для этой темы. Все последующие шаги будут методами и функциями, которые должны быть размещены внутри этого класса. Мы также определим наши переменные здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
package licksquid.movement;
 
import android.graphics.Canvas;
import android.view.SurfaceHolder;
 
public class UpdateThread extends Thread {
    private long time;
    private final int fps = 20;
    private boolean toRun = false;
    private MovementView movementView;
    private SurfaceHolder surfaceHolder;
 
}

Основная цель этого конструктора — заполнить переменную surfaceHolder, которая в конечном итоге будет использоваться для предоставления ссылки на Canvas.

1
2
3
4
public UpdateThread(MovementView rMovementView) {
    movementView = rMovementView;
    surfaceHolder = movementView.getHolder();
}

Этот метод служит одной простой, но важной цели: дать потоку разрешение на запуск или не запускаться.

1
2
3
public void setRunning(boolean run) {
    toRun = run;
}

Это основной метод Thread. Код в этом методе диктует, что делается с каждым тиком потока. Это список задач, которые он выполняет:

  • Проверьте, есть ли у него разрешение на запуск.
  • Если это так, проверьте, прошло ли требуемое время, чтобы соответствовать значению FPS (кадров в секунду).
  • Если это так, установите холст на пустой.
  • Получить ссылку на холст и заблокировать его, чтобы подготовиться к рисованию.
  • Обновите физику мяча.
  • Нарисуйте мяч в новой позиции.
  • Если это безопасно, заблокируйте и обновите холст.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void run() {
 
    Canvas c;
    while (toRun) {
 
        long cTime = System.currentTimeMillis();
 
        if ((cTime — time) <= (1000 / fps)) {
 
            c = null;
            try {
                c = surfaceHolder.lockCanvas(null);
 
                movementView.updatePhysics();
                movementView.onDraw(c);
            } finally {
                if (c != null) {
                    surfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
        time = cTime;
    }
}

Ваш файл UpdateThread теперь должен выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package example.movement;
 
import android.graphics.Canvas;
import android.view.SurfaceHolder;
 
public class UpdateThread extends Thread {
 
    private long time;
    private final int fps = 20;
    private boolean toRun = false;
    private MovementView movementView;
    private SurfaceHolder surfaceHolder;
 
    public UpdateThread(MovementView rMovementView) {
        movementView = rMovementView;
        surfaceHolder = movementView.getHolder();
    }
 
    public void setRunning(boolean run) {
        toRun = run;
    }
 
    @Override
    public void run() {
        Canvas c;
        while (toRun) {
             
            long cTime = System.currentTimeMillis();
 
            if ((cTime — time) <= (1000 / fps)) {
 
                c = null;
                try {
                    c = surfaceHolder.lockCanvas(null);
                    movementView.updatePhysics();
                    movementView.onDraw(c);
                } finally {
 
                    if (c != null) {
                        surfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }
            time = cTime;
        }
    }
}

И это все, что нам нужно сделать. Если вы запустите приложение сейчас, вы должны увидеть шар, подпрыгивающий на экране!

По общему признанию, это было упрощенным введением в достижение движения в приложении для Android. Тем не менее, я считаю, что, как только вы поймете основные понятия всего этого учебного пособия, очень легко перейти к гораздо более сложному сценарию.

Например, чтобы создать космических захватчиков, таких как игра, вы можете поместить всех инопланетян, просеивающих из стороны в сторону, в один поток, который на базовом уровне функционирует так же, как тот, что представлен здесь. Надеюсь, вы нашли этот урок полезным! 🙂