После рассмотрения жизненного цикла игры сразу стоит рассмотреть архитектуру (каркас). Вообще Роллингс и Моррис (Rollings and Dave Morris) в своей книге «Game Architecture and Design» подробно описывают создание игр с точки зрения архитектуры. В своё время я правда не особо проникся этой книгой, но вам может понравится. Я же опишу архитектуру, которую стараюсь использовать сам.
Разбиение приложения на компоненты со слабым связыванием — это не просто какой-то идеологических ход, такой подход действительно очень упрощает разработку. В частности, я предлагаю использовать заезженный паттерн — MVC. Часть материала брал с занятного сайта http://obviam.net/. Там вообще очень много полезной информации для разработчиков игр.
MVC
Довольно-таки удобный образец архитектуры для разработки игр. Главным его преимуществом, как по мне, является то, что можно вносить изменения в какую-то часть игры, при этом не затрагивая остальные компоненты приложения.
Примерно как всё в играх происходит? Игрок производит какую-то манипуляцию:
- Игрок нажимает на экран (или на клавиатуру).
- В controller обрабатывается нажатие. Здесь же по сути вся логика реализована: проверка на препятствия, отслеживание состояний объектов, изменение их состояний и т.д.
- То есть, controller изменяет состояние объектов (model‘s).
- После чего объекты отрисовываются (view).
MVC очень удачно подходит. Если ещё не поняли, поясню кое-какие моменты. Объекты (Model) абсолютно ничего не знают про рендеринг. Многие пишут, что объекты не должны и состояние менять сами, а за них это должен делать контроллер. Я к этому вопросы подошёл практически. Возьмите, к примеру, вашего персонажа, которому надо как-то описать логику обхода препятствий. Большинство скажет, что в контроллере сие дело надо реализовывать. Но почему? Ведь, когда вы идёте по улице, то обходите препятствие после собственных расчётов, а не мир или контроллер просчитывает это дело. Так что, часть логики взаимодействия с миром я бы посоветовал именно в сами объекты добавлять.
Я имею ввиду именно живые объекты (если можно так сказать про виртуальных персонажей (: ). Логику неодушевлённых предметов можно и в контроллере делать. Перейдём к практической части. За основу берём проект из введения.
Игровые компоненты
В данной статье покажу как разбить на части приложение. Немного про объекты мира расскажу.
Создание мира и его объектов
Добавьте к проекту package suvitruf.libgdxtutorial.model. Здесь у нас будут объекты мира. Добавьте в этот пакет класс Brick
и зададим базовые свойства:
package suvitruf.libgdxtutorial.model; //импорт нужных либ import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Brick { //размер объекта static final float SIZE = 1f; //координаты Vector2 position = new Vector2(); Rectangle bounds = new Rectangle(); public Brick(Vector2 pos) { this.position = pos; this.bounds.width = SIZE; this.bounds.height = SIZE; } public Rectangle getBounds() { return bounds; } public Vector2 getPosition() { return position; } }
У блока нет никакой логики, он представляет собой…ммм…просто кирпич. Он ни с чем не взаимодействует, но с ним могут взаимодействовать живые объекты. Мы используем тип Vector2
от libgdx. Это позволяет нам работать лишь с Евклидовыми векторами. Мы будем использовать векторы для позиционирования, вычисления скорости и для движения (ну да, кирпич не двигается…но наш персонаж будет).
Далее добавим класс (добавьте класс Player
к пакету suvitruf.libgdxtutorial.model), который будет являть собой нашего персонажа.
package suvitruf.libgdxtutorial.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Player { //состояние public enum State { NONE, WALKING, DEAD } //скорость движения public static final float SPEED = 2f; //размер public static final float SIZE = 0.7f; //позиция в мире Vector2 position = new Vector2(); //используется для вычисления движения Vector2 velocity = new Vector2(); //прямоугольник, в который вписан игрок //будет использоваться в будущем для нахождения коллизий (столкновение со стенкой и т.д. Rectangle bounds = new Rectangle(); //текущее состояние State state = State.NONE; public Player(Vector2 position) { this.position = position; this.bounds.height = SIZE; this.bounds.width = SIZE; } public Rectangle getBounds() { return bounds; } public Vector2 getVelocity() { return velocity; } public Vector2 getPosition() { return position; } //обновления движения public void update(float delta) { position.add(velocity.tmp().mul(delta)); } }
Теперь нам нужно создать мир, в котором будут все эти объекты. Добавляем в пакет suvitruf.libgdxtutorial.model класс World
. Мир условно делится на клетки. К примеру создадим мир 8×5.
package suvitruf.libgdxtutorial.model; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; public class World { //массив блоков Array<Brick> bricks = new Array<Brick>(); //наш персонаж public Player player; //ширина мира public int width; //высота мира public int height; //получить массив блоков public Array<Brick> getBricks() { return bricks; } //получить игрока public Player getPlayer() { return player; } public World() { width = 8; height = 5; createWorld(); } //создадим тестовый мир какой-нибудь public void createWorld() { player = new Player(new Vector2(6,2)); bricks.add(new Brick(new Vector2(0, 0))); bricks.add(new Brick(new Vector2(1, 0))); bricks.add(new Brick(new Vector2(2, 0))); bricks.add(new Brick(new Vector2(3, 0))); bricks.add(new Brick(new Vector2(4, 0))); bricks.add(new Brick(new Vector2(5, 0))); bricks.add(new Brick(new Vector2(6, 0))); bricks.add(new Brick(new Vector2(7, 0))); } }
World
— модель мира. По сути он является контейнером для объектов, что логично (:
Контроллер
Создадим пакет suvitruf.libgdxtutorial.controller и добавим в него класс WorldController
. В этом классе как раз и будет реализована логика вся. По идеи в контроллере производятся изменения состояний объектов мира. И главное, в зависимости от действий юзера будут манипуляции с объектом Player
.
package suvitruf.libgdxtutorial.controller; import java.util.HashMap; import java.util.Map; import suvitruf.libgdxtutorial.model.*; public class WorldController { //направление движения enum Keys { LEFT, RIGHT, UP, DOWN } //игрок public Player player; //куда движемся...игрок может двигаться одновременно по 2-м направлениям static Map<Keys, Boolean> keys = new HashMap<WorldController.Keys, Boolean>(); //первоначально стоим static { keys.put(Keys.LEFT, false); keys.put(Keys.RIGHT, false); keys.put(Keys.UP, false); keys.put(Keys.DOWN, false); }; public WorldController(World world) { this.player = world.getPlayer(); } //флаг устанавливаем, что движемся влево public void leftPressed() { keys.get(keys.put(Keys.LEFT, true)); } //флаг устанавливаем, что движемся вправо public void rightPressed() { keys.get(keys.put(Keys.RIGHT, true)); } //флаг устанавливаем, что движемся вверх public void upPressed() { keys.get(keys.put(Keys.UP, true)); } //флаг устанавливаем, что движемся вниз public void downPressed() { keys.get(keys.put(Keys.DOWN, true)); } //освобождаем флаги public void leftReleased() { keys.get(keys.put(Keys.LEFT, false)); } public void rightReleased() { keys.get(keys.put(Keys.RIGHT, false)); } public void upReleased() { keys.get(keys.put(Keys.UP, false)); } public void downReleased() { keys.get(keys.put(Keys.DOWN, false)); } //главный метод класса...обновляем состояния объектов здесь public void update(float delta) { processInput(); player.update(delta); } public void resetWay(){ rightReleased(); leftReleased(); downReleased(); upReleased(); } //в зависимости от выбранного направления движения выставляем новое направление движения для персонажа private void processInput() { if (keys.get(Keys.LEFT)) player.getVelocity().x = -Player.SPEED; if (keys.get(Keys.RIGHT)) player.getVelocity().x =Player.SPEED; if (keys.get(Keys.UP)) player.getVelocity().y = Player.SPEED; if (keys.get(Keys.DOWN)) player.getVelocity().y = -Player.SPEED; //если не выбрано направление, то стоим на месте if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) || (!keys.get(Keys.LEFT) && (!keys.get(Keys.RIGHT)))) player.getVelocity().x = 0; if ((keys.get(Keys.UP) && keys.get(Keys.DOWN)) || (!keys.get(Keys.UP) && (!keys.get(Keys.DOWN)))) player.getVelocity().y = 0; } }
Рендеринг
И так, про контроллер и объекты мира поговорили. Теперь нужно отрисовать объекты наши. Для этого создадим пакет suvitruf.libgdxtutorial.view, а в нём класс WorldRenderer
.
package suvitruf.libgdxtutorial.view; import suvitruf.libgdxtutorial.model.*; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { public static float CAMERA_WIDTH = 8f; public static float CAMERA_HEIGHT = 5f; private World world; public OrthographicCamera cam; ShapeRenderer renderer = new ShapeRenderer(); public int width; public int height; public float ppuX; // пикселей на точку мира по X public float ppuY; // пикселей на точку мира по Y public void setSize (int w, int h) { this.width = w; this.height = h; ppuX = (float)width / CAMERA_WIDTH; ppuY = (float)height / CAMERA_HEIGHT; } //установка камеры public void SetCamera(float x, float y){ this.cam.position.set(x, y,0); this.cam.update(); } public WorldRenderer(World world) { this.world = world; this.cam = new OrthographicCamera(CAMERA_WIDTH, CAMERA_HEIGHT); //устанавливаем камеру по центру SetCamera(CAMERA_WIDTH / 2f, CAMERA_HEIGHT / 2f); } //основной метод, здесь мы отрисовываем все объекты мира public void render() { drawBricks(); drawPlayer() ; } //отрисовка кирпичей private void drawBricks() { renderer.setProjectionMatrix(cam.combined); //тип устанавливаем...а данном случае с заливкой renderer.begin(ShapeType.FilledRectangle); //прогоняем блоки for (Brick brick : world.getBricks()) { Rectangle rect = brick.getBounds(); float x1 = brick.getPosition().x + rect.x; float y1 = brick.getPosition().y + rect.y; renderer.setColor(new Color(0, 0, 0, 1)); //и рисуем блоки renderer.filledRect(x1, y1, rect.width, rect.height); } renderer.end(); } //отрисовка персонажа по аналогии private void drawPlayer() { renderer.setProjectionMatrix(cam.combined); Player player = world.getPlayer(); renderer.begin(ShapeType.Rectangle); Rectangle rect = player.getBounds(); float x1 = player.getPosition().x + rect.x; float y1 = player.getPosition().y + rect.y; renderer.setColor(new Color(1, 0, 0, 1)); renderer.rect(x1, y1, rect.width, rect.height); renderer.end(); } }
ppuX
и ppuY
очень важны…Ведь мир у нас 8на5, а экран телефона в пикслеях не соответствует этим размерам. Поэтому нужны эти переменные, которые при рендеринге будут корректировать координаты объектов для вывода на реальный экран телефона.
OrthographicCamera cam
— камера, которая используется для того, чтобы «посмотреть» на мир. В текущем примере мир очень маленький, и он влезает в камеру, но когда у нас будет большой уровень, и персонаж будет перемещается в нем, мы должны будем менять положение камеры. Собственно, там и расчёт координат изменится…В будущих статьях остановлюсь на этом.
GameScreen
Теперь осталось лишь связать все наши компоненты вместе. Для этого создадим пакет suvitruf.libgdxtutorial.screens, а в нём класс GameScreen
.
GameScreen
реализует интерфейс Screen, который очень походит на ApplicationListener, но у этого есть 2 важных ключевых отличия (два метода).
show()
– вызывается, когда становится активным.
hide()
– вызывается, когда активным становится другой экран.
package suvitruf.libgdxtutorial.screens; import suvitruf.libgdxtutorial.model.*; import suvitruf.libgdxtutorial.controller.*; import suvitruf.libgdxtutorial.view.*; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.Screen; import com.badlogic.gdx.Application.ApplicationType; import com.badlogic.gdx.graphics.GL10; public class GameScreen implements Screen, InputProcessor { private World world; private WorldRenderer renderer; private WorldController controller; private int width, height; @Override public void show() { world = new World(); renderer = new WorldRenderer(world); controller = new WorldController(world); Gdx.input.setInputProcessor(this); } @Override public boolean touchDragged(int x, int y, int pointer) { ChangeNavigation(x,y); return false; } public boolean touchMoved(int x, int y) { return false; } @Override public boolean mouseMoved(int x, int y) { return false; } @Override public boolean keyTyped(char character) { return false; } @Override public void resize(int width, int height) { renderer.setSize(width, height); this.width = width; this.height = height; } @Override public void hide() { Gdx.input.setInputProcessor(null); } @Override public void pause() { } @Override public void resume() { } @Override public void dispose() { Gdx.input.setInputProcessor(null); } @Override public boolean keyDown(int keycode) { return true; } @Override public void render(float delta) { Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); controller.update(delta); renderer.render(); } @Override public boolean keyUp(int keycode) { return true; } private void ChangeNavigation(int x, int y){ controller.resetWay(); if(height-y > controller.player.getPosition().y * renderer.ppuY) controller.upPressed(); if(height-y < controller.player.getPosition().y * renderer.ppuY) controller.downPressed(); if ( x< controller.player.getPosition().x * renderer.ppuX) controller.leftPressed(); if (x> (controller.player.getPosition().x +Player.SIZE)* renderer.ppuX) controller.rightPressed(); } @Override public boolean touchDown(int x, int y, int pointer, int button) { if (!Gdx.app.getType().equals(ApplicationType.Android)) return false; ChangeNavigation(x,y); return true; } @Override public boolean touchUp(int x, int y, int pointer, int button) { if (!Gdx.app.getType().equals(ApplicationType.Android)) return false; controller.resetWay(); return true; } @Override public boolean scrolled(int amount) { return false; } }
Этот класс отзывается на действия юзера. По названиям методов, думаю, понятно всё. В ChangeNavigation()
мы определяем и устанавливаем направление движения персонажа. В данном примере, если вы кликаете левее игрока, то двигаетесь влево, если вверх то вверх и т.д.
Если запустите игру, то увидите что-то такое. Персонаж (красный квадратик) будет двигаться в зависимости от того, куда вы кликните относительно него.
Собственно всё. Можете скачать исходники урока Libgdxtutorial-lesson2.rar.