Статьи

Создайте нисходящую RPG в Flixel: ваша первая комната

В этом уроке мы не будем спрашивать, что такое Flixel? чтобы иметь внутреннюю комнату и управляемого клавиатурой персонажа в стиле ролевых игр сверху вниз (думаю, Зельда).


Давайте посмотрим на конечный результат, к которому мы будем стремиться:


Для визуальных людей среди нас, давайте посмотрим, как все будет организовано, чтобы остальное имело смысл.

список всех исходных файлов и папок, доступных в проекте

По сути, все наши изображения хранятся в папке assets а все файлы ActionScript хранятся в папке src . Если вы хотите использовать это руководство в качестве основы для своего собственного игрового движка, в папке topdown содержатся общие сведения (так называемый движок), а в папке tutorial показано, как его использовать.

Вы, вероятно, заметите довольно быстро, что у художественных файлов действительно длинные имена. Вместо того, чтобы показывать вам учебник, заполненный убедительными красными прямоугольниками (вершина моих художественных способностей), мы будем использовать некоторые художественные работы с открытым исходным кодом из OpenGameArt . Каждый файл назван так, чтобы показать источник, исполнителя и лицензию. Так, например, armor (opengameart - Redshrike - ccby30).png означает, что это изображение брони, загруженное из OpenGameArt, созданное художником, известным как Redshrike , и использующее лицензию CC-BY-30 ( Creative Commons Attribution ).

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

Вот описание каждого исходного файла в проекте:

  • topdown/TopDownEntity.as — базовый класс для любых подвижных спрайтов в нашей нисходящей RPG
  • topdown/TopDownLevel.as — базовый класс для нисходящего уровня RPG
  • tutorial/Assets.as — импортирует любые изображения, которые нам нужны в этом уроке
  • tutorial/IndoorHouseLevel.as — определяет внутреннюю комнату с несколькими объектами, лежащими вокруг
  • tutorial/Player.as — управляемый с клавиатуры, анимированный рейнджер
  • tutorial/PlayState.as — состояние Flixel, управляющее нашей игрой
  • Default.css — пустой файл, необходимый для того, чтобы компилятор Flex не предупреждал нас
  • Main.as — точка входа для приложения
  • Preloader.as — предварительный Preloader.as Flixel

Теперь приступим к делу!


Flixel — это 2D игровой движок для ActionScript 3. Процитирую домашнюю страницу:

Flixel — это библиотека для создания игр с открытым исходным кодом, которая полностью бесплатна для личного или коммерческого использования.

Самое важное, что нужно знать о Flixel, это то, что он предназначен для использования растровых изображений (растровой графики) вместо векторной графики в стиле Flash. Вы можете использовать Flash ролики, но это займет немного массажа. Поскольку сегодня я не хочу делать массаж, мы будем использовать изображения для всего нашего искусства.

Flixel поставляется с инструментом, который создает фиктивный проект для вас. Этот инструмент создает три файла, которые находятся в корне нашего проекта: Default.css , Main.as и Preloader.as . Эти три файла составляют основу практически любого проекта в Flixel. Поскольку Default.css только для того, чтобы избежать предупреждения компилятора, давайте взглянем на Main.as

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package
{
    import org.flixel.*;
    import tutorial.*;
 
    [SWF(width=»480″, height=»480″, backgroundColor=»#ffffff»)]
    [Frame(factoryClass=»Preloader»)]
    public class Main extends FlxGame
    {
        /**
         * Constructor
         */
        public function Main() {
            super(240, 240, PlayState, 2);
        }
    }
}

Здесь есть только три важные линии. Прежде всего, мы говорим Flash использовать окно 480×480 с белым фоном. Затем мы говорим Flash использовать наш класс Preloader во время загрузки. Наконец, мы говорим Flixel использовать окно размером 240×240 (с увеличением в 2 раза, чтобы все выглядело больше) и использовать PlayState когда все будет готово.

Позвольте мне кратко рассказать о состояниях Фликселя. В Flixel состояния похожи на окна, но вы можете иметь только одно за раз. Так, например, у вас может быть состояние главного меню вашей игры ( MainMenu ), и когда пользователь нажимает кнопку « Start Game , вы переключаетесь на PlayState . Поскольку мы хотим, чтобы наша игра начиналась немедленно, нам нужно только одно состояние ( PlayState ).

Далее идет Preloader.as .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
package
{
    import org.flixel.system.FlxPreloader;
 
    public class Preloader extends FlxPreloader
    {
        /**
         * Constructor
         */
        public function Preloader():void {
            className = «Main»;
            super();
        }
    }
}

Не так много, чтобы увидеть здесь. Поскольку мы FlxPreloader из FlxPreloader , Flixel действительно об этом позаботится. Единственное, на что следует обратить внимание: если вы измените Main на какое-то другое имя, вам придется изменить className здесь, на выделенной строке.

Мы почти готовы увидеть что-то на экране. Все, что нам нужно, это состояние Flixel, чтобы начать движение, так что вот PlayState.as .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package tutorial
{
    import org.flixel.*;
 
    /**
     * State for actually playing the game
     * @author Cody Sandahl
     */
    public class PlayState extends FlxState
    {
        /**
         * Create state
         */
        override public function create():void {
            FlxG.mouse.show();
        }
    }
}

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


Теперь, когда у нас есть Flixel, пришло время сделать нисходящий уровень RPG. Мне нравится давать вам повторно используемые классы, чтобы вы могли создавать свои собственные уровни, поэтому мы на самом деле создадим класс общего уровня, который мы можем использовать, чтобы сделать что-то более интересное позже. Это topdown/TopDownLevel.as .

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
package topdown
{
    import org.flixel.*;
    /**
     * Base class for all levels
     * @author Cody Sandahl
     */
    public class TopDownLevel extends FlxGroup
    {
        /**
         * Map
         */
        public var state:FlxState;
        public var levelSize:FlxPoint;
        public var tileSize:FlxPoint;
        public var numTiles:FlxPoint;
        public var floorGroup:FlxGroup;
        public var wallGroup:FlxGroup;
        public var guiGroup:FlxGroup;
         
        /**
         * Player
         */
        public var player:TopDownEntity;
        public var playerStart:FlxPoint = new FlxPoint(120, 120);
         
        /**
         * Constructor
         * @param state State displaying the level
         * @param levelSize Width and height of level (in pixels)
         * @param blockSize Default width and height of each tile (in pixels)
         */
        public function TopDownLevel(state:FlxState, levelSize:FlxPoint, tileSize:FlxPoint):void {
            super();
            this.state = state;
            this.levelSize = levelSize;
            this.tileSize = tileSize;
            if (levelSize && tileSize)
                this.numTiles = new FlxPoint(Math.floor(levelSize.x / tileSize.x), Math.floor(levelSize.y / tileSize.y));
            // setup groups
            this.floorGroup = new FlxGroup();
            this.wallGroup = new FlxGroup();
            this.guiGroup = new FlxGroup();
            // create the level
            this.create();
        }
         
        /**
         * Create the whole level, including all sprites, maps, blocks, etc
         */
        public function create():void {
            createMap();
            createPlayer();
            createGUI();
            addGroups();
            createCamera();
        }
         
        /**
         * Create the map (walls, decals, etc)
         */
        protected function createMap():void {
        }
         
        /**
         * Create the player, bullets, etc
         */
        protected function createPlayer():void {
            player = new TopDownEntity(playerStart.x, playerStart.y);
        }
         
        /**
         * Create text, buttons, indicators, etc
         */
        protected function createGUI():void {
        }
         
        /**
         * Decide the order of the groups.
         */
        protected function addGroups():void {
            add(floorGroup);
            add(wallGroup);
            add(player);
            add(guiGroup);
        }
         
        /**
         * Create the default camera for this level
         */
        protected function createCamera():void {
            FlxG.worldBounds = new FlxRect(0, 0, levelSize.x, levelSize.y);
            FlxG.camera.setBounds(0, 0, levelSize.x, levelSize.y, true);
            FlxG.camera.follow(player, FlxCamera.STYLE_TOPDOWN);
        }
         
        /**
         * Update each timestep
         */
        override public function update():void {
            super.update();
            FlxG.collide(wallGroup, player);
        }
    }
}

Все переменные имеют свои собственные описания в исходном коде, поэтому я не буду утомлять вас чрезмерным повторением. Я должен, однако, объяснить группы в Flixel.

Здесь определены три группы: floorGroup , wallGroup и guiGroup . Flixel использует группы для определения порядка рендеринга спрайтов (для определения того, что находится сверху, когда они перекрываются) и для обработки коллизий. Мы хотим, чтобы игрок мог ходить по полу (без столкновений), но мы также хотим, чтобы стены и объекты (безусловно, нужны столкновений), поэтому нам нужны две группы. Нам также нужна отдельная группа для нашего пользовательского интерфейса ( guiGroup ), чтобы мы могли быть уверены, что она отображается поверх всего остального.

Группы отображаются в порядке их добавления, что определяется нашей addGroups() . Поскольку мы хотим, чтобы guiGroup всегда была сверху, мы вызываем add(guiGroup) после всех остальных групп. Если вы создаете свои собственные группы и забываете вызывать add() , они не будут отображаться на экране.

В нашем конструкторе мы храним некоторые полезные значения (например, количество плиток на уровне) и вызываем create() . Функция create() показывает, что входит в уровень Flixel — карту, игрока, интерфейс, группы (для управления порядком рендеринга и столкновений) и вид с камеры. У каждого из них есть своя собственная функция, которая помогает сделать вещи более читабельными, и поэтому мы можем повторно использовать обычные функции. Например, взгляните на createCamera() .

1
2
3
4
5
6
7
8
/**
 * Create the default camera for this level
 */
protected function createCamera():void {
    FlxG.worldBounds = new FlxRect(0, 0, levelSize.x, levelSize.y);
    FlxG.camera.setBounds(0, 0, levelSize.x, levelSize.y, true);
    FlxG.camera.follow(player, FlxCamera.STYLE_TOPDOWN);
}

Нам не нужно менять эту функцию, чтобы сделать наш собственный уровень в помещении. Flixel имеет встроенную камеру для игр сверху вниз ( FlxCamera.STYLE_TOPDOWN ). Все, что мы на самом деле делаем здесь, это говорим камере не выходить из уровня (вызывая setBounds() ) и сообщая камере следовать за игроком (вызывая follow() ), если уровень больше экрана и требует прокрутки. Это будет работать практически для любого уровня, поэтому мы можем оставить его здесь, а не перекодировать его для каждого из наших уровней.

Единственное, на что следует обратить внимание — в update() .

1
2
3
4
5
6
7
/**
 * Update each timestep
 */
override public function update():void {
    super.update();
    FlxG.collide(wallGroup, player);
}

FlxG.collide(wallGroup, player) заставляет игрока FlxG.collide(wallGroup, player) в стены, а не проходить через них. Так как мы не вызываем FlxG.collide(floorGroup, player) , игрок может ходить по всем этажам без видимых столкновений (тоже самое и для guiGroup ).

Наконец, нам нужно заставить PlayState использовать наш модный уровень.

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
package tutorial
{
    import org.flixel.*;
    import topdown.*;
 
    /**
     * State for actually playing the game
     * @author Cody Sandahl
     */
    public class PlayState extends FlxState
    {
        /**
         * Constants
         */
        public static var LEVEL_SIZE:FlxPoint = new FlxPoint(240, 240);
        public static var BLOCK_SIZE:FlxPoint = new FlxPoint(16, 16);
         
        /**
         * Current level
         * NOTE: «public static» allows us to get info about the level from other classes
         */
        public static var LEVEL:TopDownLevel = null;
         
        /**
         * Create state
         */
        override public function create():void {
            FlxG.mouse.show();
            // load level
            LEVEL = new TopDownLevel(this, LEVEL_SIZE, BLOCK_SIZE);
            this.add(LEVEL);
        }
    }
}

Не забудьте назвать this.add(LEVEL) если вы не хотите смотреть на черный экран вечно. Как говорится в комментарии, я использовал public static var LEVEL для удобства в будущем. Предположим, вы добавили в свою игру искусственный интеллект, и ваш ИИ должен знать, где находится игрок; таким образом, вы можете вызывать PlayState.LEVEL.player и делать все просто и красиво. Это не обязательно самый красивый способ сделать что-то, но он будет работать, если его использовать с осторожностью.


Сущность — это то, что должно отображаться и может перемещаться. Это может быть игрок, персонаж, управляемый компьютером, или даже что-то вроде стрелки. Поскольку на уровне может быть много сущностей, нам нужен универсальный класс, который мы можем использовать, чтобы сэкономить время. Взгляните на topdown/TopDownEntity.as .

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
package topdown
{
    import org.flixel.*;
     
    /**
     * A moveable object in the game (player, enemy, NPC, etc)
     * @author Cody Sandahl
     */
    public class TopDownEntity extends FlxSprite
    {
        /**
         * Constants
         */
        public static const SIZE:FlxPoint = new FlxPoint(16, 18);
         
        /**
         * Constructor
         * @param X X location of the entity
         * @param Y Y location of the entity
         */
        public function TopDownEntity(X:Number = 100, Y:Number = 100):void {
            super(X, Y);
            makeGraphic(SIZE.x, SIZE.y, 0xFFFF0000);
        }
    }
}

Обратите внимание, что мы расширяемся от FlxSprite . Это дает нам доступ ко многим возможностям Flixel. makeGraphic() создает прямоугольное растровое изображение заданного размера (в данном случае 16×18), используя цвет, который вы передаете. Этот цвет имеет формат 0xAARRGGBB , поэтому 0xFFFF0000 означает, что мы создаем сплошную красную рамку (я предупреждал вас о своем художественном способностей). Вы можете возиться с этим значением, чтобы увидеть, как меняется цвет. На самом деле, теперь у нас есть что-то, кроме пустого экрана!

наш первый объект - черный экран с красной рамкой посередине

Все еще не слишком захватывающе, но, по крайней мере, мы можем что-то увидеть, верно?


Я не знаю как вы, но я устал смотреть на этот черный фон. Давайте сделаем так, чтобы это выглядело как комната. Вот tutorial/IndoorHouseLevel.as .

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
package tutorial
{
    import org.flixel.*;
    import topdown.*;
     
    /**
     * A basic indoor scene
     * @author Cody Sandahl
     */
    public class IndoorHouseLevel extends TopDownLevel
    {
        /**
         * Floor layer
         */
        protected static var FLOORS:Array = new Array(
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        );
         
        /**
         * Wall layer
         */
        protected static var WALLS:Array = new Array(
            1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8,
            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
        );
         
        /**
         * Constructor
         * @param state State displaying the level
         * @param levelSize Width and height of level (in pixels)
         * @param blockSize Default width and height of each tile (in pixels)
         */
        public function IndoorHouseLevel(state:FlxState, levelSize:FlxPoint, blockSize:FlxPoint):void {
            super(state, levelSize, blockSize);
        }
         
        /**
         * Create the map (walls, decals, etc)
         */
        override protected function createMap():void {
            var tiles:FlxTilemap;
            // floors
            tiles = new FlxTilemap();
            tiles.loadMap(
                FlxTilemap.arrayToCSV(FLOORS, 15), // convert our array of tile indices to a format flixel understands
                Assets.FLOORS_TILE, // image to use
                tileSize.x, // width of each tile (in pixels)
                tileSize.y, // height of each tile (in pixels)
                0, // don’t use auto tiling (needed so we can change the rest of these values)
                0, // starting index for our tileset (0 = include everything in the image)
                0, // starting index for drawing our tileset (0 = every tile is drawn)
                uint.MAX_VALUE // which tiles allow collisions by default (uint.MAX_VALUE = no collisions)
            );
            floorGroup.add(tiles);
            // walls
            // FFV: make left/right walls’ use custom collision rects
            tiles = new FlxTilemap();
            tiles.loadMap(
                FlxTilemap.arrayToCSV(WALLS, 15), // convert our array of tile indices to a format flixel understands
                Assets.WALLS_TILE, // image to use
                tileSize.x, // width of each tile (in pixels)
                tileSize.y // height of each tile (in pixels)
            );
            wallGroup.add(tiles);
        }
    }
}

Первое, что вы заметите, это два гигантских массива чисел: FLOORS и WALLS . Эти массивы определяют слои нашей карты. Числа — это индексы плиток, основанные на графике, которую мы используем. Я увеличил изображение, которое мы используем для наших стен, чтобы показать вам, о чем я говорю.

спрайт для стен на нашем уровне

Обратите внимание, что ноль пуст (ничего не рисует). Напольное изображение, с другой стороны, представляет собой всего лишь одну плитку, повторенную в данный момент. Это означает, что мы хотим нарисовать каждую плитку (включая ноль). Поэтому, если вы посмотрите на createMap() , наш код для загрузки на этаже длиннее, чем наш код для загрузки на стенах.

Мы начнем с FlxTilemap.arrayToCSV(FLOORS, 15) , который преобразует наш большой массив в формат, который нравится Flixel (CSV). Число в конце говорит Flixel, сколько значений в каждой строке. Далее мы сообщаем Flixel, какое изображение использовать ( Assets.FLOORS_TILE — я объясню, о чем это на следующем шаге). После определения размера каждого блока на изображении у нас есть еще четыре значения для нашего пола, чем для наших стен. Поскольку мы хотим, чтобы все плитки (включая ноль) были нарисованы для нашего пола, нам нужно передать эти дополнительные значения.

Единственное, что немного странно, это последнее: uint.MAX_VALUE . Каждый номер плитки (от нуля до числа плиток в нашем изображении), который выше числа, переданного по этому параметру, будет отмечен для столкновений. Все, что ниже этого числа, будет игнорировать столкновения по умолчанию. Итак, если у вас была стена, через которую мог пройти игрок, вы можете поместить ее в конец вашего изображения (высокий индекс) и использовать это значение, чтобы Flixel игнорировал столкновения. Поскольку мы никогда не хотим, чтобы с полом происходили какие-либо коллизии, мы используем uint.MAX_VALUE потому что каждый индекс uint.MAX_VALUE будет ниже этого значения и, следовательно, не будет коллизий.

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


Поскольку мы используем изображения, нам нужно сообщить Flash о них. Один из более простых способов сделать это — встроить их в свой SWF. Вот как мы это делаем в этом проекте (можно найти в tutorial/Assets.as ).

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
package tutorial
{
    import flash.utils.ByteArray;
    /**
     * Embeds and imports all assets for the game
     * @author Cody Sandahl
     */
    public class Assets
    {
        // sprites
        [Embed(source = «../../assets/sprites/ranger (opengameart — Antifarea — ccby30).png»)] public static var RANGER_SPRITE:Class;
        [Embed(source = «../../assets/sprites/rug1 (opengameart — Redshrike — ccby30).png»)] public static var RUG1_SPRITE:Class;
        [Embed(source = «../../assets/sprites/rug2 (opengameart — Redshrike — ccby30).png»)] public static var RUG2_SPRITE:Class;
        [Embed(source = «../../assets/sprites/bookcase (opengameart — Redshrike — ccby30).png»)] public static var BOOKCASE_SPRITE:Class;
        [Embed(source = «../../assets/sprites/chair_down (opengameart — Redshrike — ccby30).png»)] public static var CHAIRDOWN_SPRITE:Class;
        [Embed(source = «../../assets/sprites/chair_left (opengameart — Redshrike — ccby30).png»)] public static var CHAIRLEFT_SPRITE:Class;
        [Embed(source = «../../assets/sprites/chair_right (opengameart — Redshrike — ccby30).png»)] public static var CHAIRRIGHT_SPRITE:Class;
        [Embed(source = «../../assets/sprites/chair_up (opengameart — Redshrike — ccby30).png»)] public static var CHAIRUP_SPRITE:Class;
        [Embed(source = «../../assets/sprites/table_round (opengameart — Redshrike — ccby30).png»)] public static var TABLEROUND_SPRITE:Class;
        [Embed(source = «../../assets/sprites/armor (opengameart — Redshrike — ccby30).png»)] public static var ARMOR_SPRITE:Class;
        [Embed(source = «../../assets/sprites/bed (opengameart — Redshrike — ccby30).png»)] public static var BED_SPRITE:Class;
 
        // tiles
        [Embed(source = «../../assets/tiles/walls (opengameart — daniel siegmund — ccby30).png»)] public static var WALLS_TILE:Class;
        [Embed(source = «../../assets/tiles/floor_wood (opengameart — Redshrike — ccby30).png»)] public static var FLOORS_TILE:Class;
    }
}

Я даю вам все художественные работы сразу, потому что это не так уж сложно, как только вы овладеете им. Давайте посмотрим на выделенные строки. Здесь мы загружаем два изображения: одно для наших стен и одно для нашего пола. Если вы помните на последнем шаге, мы сказали Flixel использовать Assets.WALLS_TILE и Assets.FLOORS_TILE при загрузке в слои карты. Здесь мы определяем эти переменные.

Обратите внимание, что мы используем путь относительно файла Assets.as . Вы также можете встраивать такие вещи, как XML-файлы, SWF-файлы и множество других ресурсов. Однако нам нужны только изображения. Дополнительную информацию о встраивании ресурсов во Flash можно найти в этой статье в блоге Nightspade.

Теперь, когда у нас есть встроенные и доступные изображения, мы можем сказать PlayState.as использовать наш новомодный уровень.

1
2
3
4
5
6
7
8
9
/**
 * Create state
 */
override public function create():void {
    FlxG.mouse.show();
    // load level
    LEVEL = new IndoorHouseLevel(this, LEVEL_SIZE, BLOCK_SIZE);
    this.add(LEVEL);
}

Мы изменили выделенную строку с использования TopDownLevel на наш новый IndoorHouseLevel . Теперь, если вы запустите проект, вы увидите нечто, похожее на комнату.

Игрок на красной площади в центре

Это может быть комната, но это скучная комната. Давайте украсим это немного с некоторой мебелью. Во-первых, нам нужно еще несколько групп и несколько переменных внутри IndoorHouseLevel .

01
02
03
04
05
06
07
08
09
10
11
12
13
/**
 * Custom groups
 */
protected var decalGroup:FlxGroup;
protected var objectGroup:FlxGroup;
 
/**
 * Game objects
 */
protected var bookcase:FlxSprite;
protected var armor:FlxSprite;
protected var table:FlxSprite;
protected var bed:FlxSprite;

decalGroup позволит нам добавить несколько ковриков (чисто визуальный objectGroup ), в то время как objectGroup позволит нам добавить немного мебели, которая будет мешать игроку. Другие переменные — это предметы мебели, которые мы добавим через минуту.

Далее нам нужно добавить эти объекты на уровень. Мы добавляем выделенную строку и все под ней.

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
/**
 * Create the map (walls, decals, etc)
 */
override protected function createMap():void {
    var tiles:FlxTilemap;
    // floors
    tiles = new FlxTilemap();
    tiles.loadMap(
        FlxTilemap.arrayToCSV(FLOORS, 15), // convert our array of tile indices to a format flixel understands
        Assets.FLOORS_TILE, // image to use
        tileSize.x, // width of each tile (in pixels)
        tileSize.y, // height of each tile (in pixels)
        0, // don’t use auto tiling (needed so we can change the rest of these values)
        0, // starting index for our tileset (0 = include everything in the image)
        0, // starting index for drawing our tileset (0 = every tile is drawn)
        uint.MAX_VALUE // which tiles allow collisions by default (uint.MAX_VALUE = no collisions)
    );
    floorGroup.add(tiles);
    // walls
    // FFV: make left/right walls’ use custom collision rects
    tiles = new FlxTilemap();
    tiles.loadMap(
        FlxTilemap.arrayToCSV(WALLS, 15), // convert our array of tile indices to a format flixel understands
        Assets.WALLS_TILE, // image to use
        tileSize.x, // width of each tile (in pixels)
        tileSize.y // height of each tile (in pixels)
    );
    wallGroup.add(tiles);
    // objects
    createObjects();
}
 
/**
 * Add all the objects, obstacles, etc to the level
 */
protected function createObjects():void {
    var sprite:FlxSprite;
    // create custom groups
    decalGroup = new FlxGroup();
    objectGroup = new FlxGroup();
    // decals (decorative elements that have no functionality)
    sprite = new FlxSprite(
        16, // x location
        16, // y location
        Assets.RUG1_SPRITE // image to use
    );
    decalGroup.add(sprite);
     
    sprite = new FlxSprite(
        11 * tileSize.x, // x location (using tileSize to align it with a tile)
        1.5 * tileSize.y, // y location (showing that you don’t need to line up with a tile)
        Assets.RUG2_SPRITE // image to use
    );
    decalGroup.add(sprite);
    // objects and obstacles
    // NOTE: this group gets tested for collisions
    bookcase = new FlxSprite(
        32, // x location
        0, // y location (showing that you can overlap with the walls if you want)
        Assets.BOOKCASE_SPRITE // image to use
    );
    bookcase.immovable = true;
    objectGroup.add(bookcase);
     
    table = new FlxSprite(192, 192, Assets.TABLEROUND_SPRITE);
    table.immovable = true;
    objectGroup.add(table);
     
    sprite = new FlxSprite(176, 192, Assets.CHAIRRIGHT_SPRITE);
    sprite.immovable = true;
    objectGroup.add(sprite);
     
    sprite = new FlxSprite(216, 192, Assets.CHAIRLEFT_SPRITE);
    sprite.immovable = true;
    objectGroup.add(sprite);
     
    armor = new FlxSprite(192, 0, Assets.ARMOR_SPRITE);
    armor.immovable = true;
    objectGroup.add(armor);
     
    bed = new FlxSprite(16, 192, Assets.BED_SPRITE);
    bed.immovable = true;
    objectGroup.add(bed);
}

Я использую дополнительную функцию createObjects() , чтобы было легче читать. Комментарии объясняют каждый отдельный объект, но позвольте мне предложить несколько общих замечаний. Во-первых, нам всегда нужно не забывать вызывать add() для каждого объекта, иначе он не будет отображаться. Кроме того, нам нужно использовать правильную группу ( mapGroup , floorGroup , decalGroup , objectGroup и т. Д.) При вызове add (), иначе это испортит наш порядок рендеринга и обнаружение столкновений.

Также обратите внимание на все возможные способы размещения наших предметов и надписей. Мы можем жестко закодировать значения (как мы делаем с первым ковром), мы можем использовать tileSize чтобы выровнять его с плитками пола и стены (как мы делаем со вторым ковром), и мы можем смешивать и сопоставлять содержание нашего сердца. Просто знайте, что Flixel не обнаружит это, если мы поместим что-то за уровень или перекрываем другой объект — это предполагает, что мы знаем, что делаем.

Теперь нам нужно отобразить наши новые группы в правильном порядке и обработать коллизии. Добавьте эти функции в IndoorHouseLevel .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 * Decide the order of the groups.
 */
override protected function addGroups():void {
    add(floorGroup);
    add(wallGroup);
    add(decalGroup);
    add(objectGroup);
    add(player);
    add(guiGroup);
}
 
/**
 * Update each timestep
 */
override public function update():void {
    super.update();
    FlxG.collide(objectGroup, player);
}

Поскольку мы хотим, чтобы наши новые группы отображались поверх этажей и стен, нам необходимо полностью заново выполнить addGroups() которая была у нас в TopDownLevel . Нам также нужно добавить обнаружение столкновений для нашей мебели в objectGroup . Еще раз, так как мы не вызываем FlxG.collide() для decalGroup , игрок не будет блокирован нашими внушительными ковриками. Теперь наша комната выглядит немного менее свободной.

Мебель для гостиной на деревянной площади, в каменной стене

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package tutorial
{
    import org.flixel.*;
    import topdown.*;
    /**
     * Player-controlled entity
     * @author Cody Sandahl
     */
    public class Player extends TopDownEntity
    {
        /**
         * Constructor
         * @param X X location of the entity
         * @param Y Y location of the entity
         */
        public function Player(X:Number=100, Y:Number=100):void {
            super(X, Y);
        }
    }
}

Это скелет, который мы будем использовать, чтобы придать более интересный вид игроку. Теперь, когда у нас есть собственный проигрыватель, нам нужно использовать его в IndoorHouseLevel . Добавьте эту функцию в конце класса.

1
2
3
4
5
6
/**
 * Create the player
 */
override protected function createPlayer():void {
    player = new Player(playerStart.x, playerStart.y);
}

Это изменилось с использования TopDownEntity на использование Player . Теперь давайте заставим эту красную коробку двигаться.


Поскольку мы могли бы хотеть, чтобы объекты, отличные от Player, могли перемещаться, мы собираемся добавить некоторые функции в TopDownEntity . Вот новая версия.

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
package topdown
{
    import org.flixel.*;
     
    /**
     * A moveable object in the game (player, enemy, NPC, etc)
     * @author Cody Sandahl
     */
    public class TopDownEntity extends FlxSprite
    {
        /**
         * Constants
         */
        public static const SIZE:FlxPoint = new FlxPoint(16, 18);
        public static const RUNSPEED:int = 80;
         
        /**
         * Constructor
         * @param X X location of the entity
         * @param Y Y location of the entity
         */
        public function TopDownEntity(X:Number = 100, Y:Number = 100):void {
            super(X, Y);
            makeGraphic(SIZE.x, SIZE.y, 0xFFFF0000);
            // movement
            maxVelocity = new FlxPoint(RUNSPEED, RUNSPEED);
            drag = new FlxPoint(RUNSPEED * 4, RUNSPEED * 4);
        }
         
        /**
         * Update each timestep
         */
        public override function update():void {
            updateControls();
            super.update();
        }
         
        /**
         * Check keyboard/mouse controls
         */
        protected function updateControls():void {
            acceleration.x = acceleration.y = 0;
        }
         
        /**
         * Move entity left
         */
        public function moveLeft():void {
            facing = LEFT;
            acceleration.x = -RUNSPEED * 4;
        }
         
        /**
         * Move entity right
         */
        public function moveRight():void {
            facing = RIGHT;
            acceleration.x = RUNSPEED * 4;
        }
         
        /**
         * Move entity up
         */
        public function moveUp():void {
            facing = UP;
            acceleration.y = -RUNSPEED * 4;
        }
         
        /**
         * Move playe rdown
         */
        public function moveDown():void {
            facing = DOWN;
            acceleration.y = RUNSPEED * 4;
        }
    }
}

Мы добавили новую константу RUNSPEED , которая определяет, как быстро движутся наши объекты. Затем мы устанавливаем maxVelocity и drag (замедление) в нашем конструкторе. После этого мы вызываем updateControls() каждый кадр, чтобы мы могли проверять наличие клавиатуры, мыши или AI (в зависимости от наших потребностей). Наконец, мы добавляем некоторые вспомогательные функции для перемещения в каждом направлении. Обратите внимание, что мы обновляем facing в каждом из них. Это удобный способ узнать, какую анимацию использовать позже.

Теперь нам нужно использовать клавиатуру внутри Player . Добавьте эту функцию после конструктора.

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
/**
 * Check for user input to control this entity
 */
override protected function updateControls():void {
    super.updateControls();
    // check keys
    // NOTE: this accounts for someone pressing multiple arrow keys at the same time (even in opposite directions)
    var movement:FlxPoint = new FlxPoint();
    if (FlxG.keys.pressed(«LEFT»))
        movement.x -= 1;
    if (FlxG.keys.pressed(«RIGHT»))
        movement.x += 1;
    if (FlxG.keys.pressed(«UP»))
        movement.y -= 1;
    if (FlxG.keys.pressed(«DOWN»))
        movement.y += 1;
    // check final movement direction
    if (movement.x < 0)
        moveLeft();
    else if (movement.x > 0)
        moveRight();
    if (movement.y < 0)
        moveUp();
    else if (movement.y > 0)
        moveDown();
}

Поэтому каждый кадр мы проверяем, какие клавиши нажимаются. Flixel позволяет нам тестировать ключи по-разному. Здесь мы используем pressed() , которое true до тех пор, пока нажата клавиша. Если бы мы использовали justPressed() , это было бы true только после того, как игрок нажал клавишу, даже если после этого удерживать клавишу нажатой. Это было бы наоборот, если бы мы использовали justReleased() .

Как я утверждаю в комментариях, я хочу обработать случай, когда пользователь нажимает влево и вправо (например) одновременно, не двигаясь. Увеличение или уменьшение элемента motion.x в зависимости от того, какая стрелка нажата, позволяет нам сделать это, потому что movement.x был бы нулевым, если бы и левый, и правый были нажаты.

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

Вы, вероятно, заметите, что коробка не доходит до левой и правой стен. Это более эзотерический аспект Flixel. Flixel использует довольно простое обнаружение столкновений (но позволяет нам сделать его более сложным, если мы хотим). Поскольку изображения, которые мы используем для всех стен, имеют одинаковый размер (16×16), Flixel использует 16×16 в качестве размера столкновения, даже если большинство изображений левой и правой стен прозрачны. Исправление такого поведения выходит за рамки этого руководства, но это можно сделать.

мебель для игры в игру на красной площади

Я обещал, что мы не будем придерживаться красной рамки (хотя это мило), так что мы идем с анимированным спрайтом. Поскольку нам, вероятно, нужна возможность анимировать будущие объекты, мы будем добавлять базовую функциональность TopDownEntityвместо Player. Вот новый конструктор createAnimations()и update()функции для TopDownEntity.

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
/**
 * Constructor
 * @param X X location of the entity
 * @param Y Y location of the entity
 */
public function TopDownEntity(X:Number = 100, Y:Number = 100):void {
    super(X, Y);
    makeGraphic(SIZE.x, SIZE.y, 0xFFFF0000); // use this if you want a generic box graphic by default
    // movement
    maxVelocity = new FlxPoint(RUNSPEED, RUNSPEED);
    drag = new FlxPoint(RUNSPEED * 4, RUNSPEED * 4); // decelerate to a stop within 1/4 of a second
    // animations
    createAnimations();
}
 
/**
 * Create the animations for this entity
 * NOTE: these will be different if your art is different
 */
protected function createAnimations():void {
    addAnimation("idle_up", [1]);
    addAnimation("idle_right", [5]);
    addAnimation("idle_down", [9]);
    addAnimation("idle_left", [13]);
    addAnimation("walk_up", [0, 1, 2], 12); // 12 = frames per second for this animation
    addAnimation("walk_right", [4, 5, 6], 12);
    addAnimation("walk_down", [8, 9, 10], 12);
    addAnimation("walk_left", [12, 13, 14], 12);
    addAnimation("attack_up", [16, 17, 18, 19], 12, false); // false = don't loop the animation
    addAnimation("attack_right", [20, 21, 22, 23], 12, false);
    addAnimation("attack_down", [24, 25, 26, 27], 12, false);
    addAnimation("attack_left", [28, 29, 30, 31], 12, false);
}
 
/**
 * Update each timestep
 */
public override function update():void {
    updateControls();
    updateAnimations();
    super.update();
}

FlxSpriteПредполагается, что, если мы анимируем, у нас есть несколько кадров анимации, хранящихся в одном изображении (называемом спрайт-листом). Хотя в нашем красном поле нет рамок для анимации, изображение, которое мы будем использовать, делает. Если вы используете изображения, которые по-разному устроены в вашей собственной игре, вам необходимо изменить эти номера кадров. Кроме того, если вы посмотрите на бездействующую анимацию, вы заметите, что нам нужно передать массив индексов кадров, даже если у нас есть только один.

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

все анимации, используемые рейнджером, с помеченными номерами кадров

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

Пока все наши объекты в нашей игре используют одинаковые номера кадров, нам никогда не придется менять код анимации. Если бы наша анимация использовала различное количество кадров для анимации ходьбы и атаки, мы бы обновили соответствующие кадры addAnimation(). Так как анимации находятся в их собственной функции — createAnimations(), вы также можете переопределить эту функцию, чтобы некоторые объекты имели анимацию, отличную от остальных.

Мы также сделали еще одну новую функцию , чтобы показать правильную анимацию: updateAnimations().

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
/**
 * Based on current state, show the correct animation
 * FFV: use state machine if it gets more complex than this
 */
protected function updateAnimations():void {
    // use abs() so that we can animate for the dominant motion
    // ex: if we're moving slightly up and largely right, animate right
    var absX:Number = Math.abs(velocity.x);
    var absY:Number = Math.abs(velocity.y);
    // determine facing
    if (velocity.y < 0 && absY >= absX)
        facing = UP;
    else if (velocity.y > 0 && absY >= absX)
        facing = DOWN;
    else if (velocity.x > 0 && absX >= absY)
        facing = RIGHT;
    else if (velocity.x < 0 && absX >= absY)
        facing = LEFT
    // up
    if (facing == UP) {
        if (velocity.y != 0 || velocity.x != 0)
            play("walk_up");
        else
            play("idle_up");
    }
    // down
    else if (facing == DOWN) {
        if (velocity.y != 0 || velocity.x != 0)
            play("walk_down");
        else
            play("idle_down");
    }
    // right
    else if (facing == RIGHT) {
        if (velocity.x != 0)
            play("walk_right");
        else
            play("idle_right");
    }
    // left
    else if (facing == LEFT) {
        if (velocity.x != 0)
            play("walk_left");
        else
            play("idle_left");
    }
}

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

У нас есть только одна вещь, которую нужно сделать, прежде чем мы сможем наконец избавиться от красной коробки. Нам нужно сказать Playerиспользовать спрайт-лист рейнджера.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * Constructor
 * @param X X location of the entity
 * @param Y Y location of the entity
 */
public function Player(X:Number=100, Y:Number=100):void {
    super(X, Y);
    loadGraphic(
        Assets.RANGER_SPRITE, // image to use
        true, // animated
        false, // don't generate "flipped" images since they're already in the image
        TopDownEntity.SIZE.x, // width of each frame (in pixels)
        TopDownEntity.SIZE.y // height of each frame (in pixels)
    );
}

Мы снова идем в наш Assetsкласс, чтобы получить изображение, которое мы хотим. Комментарии говорят вам, что происходит, но позвольте мне рассказать вам немного о «перевернутых» изображениях. Вместо создания разных анимаций при перемещении влево / вправо и вверх / вниз, Flixel может просто перевернуть «правую» анимацию, чтобы сделать ее «левой», и перевернуть «вверх», чтобы сделать ее «вниз» (или наоборот). Наши анимации «вверх» и «вниз» выглядят очень по-разному (и у нас уже есть обложка со всеми направлениями), поэтому мы советуем Flixel не беспокоиться о переключении анимации.

Теперь у нас есть настоящий уровень RPG в помещении!

внутренняя комната с деревянными полами, каменными стенами, мебелью и анимированным рейнджером, бегущим по левой стороне

В качестве бонуса давайте посмотрим, как добавить элементы GUI на экран. Мы собираемся добавить простой набор инструкций вверху, чтобы пользователи знали, что делать после загрузки этого уровня. Давайте добавим графический интерфейс для IndoorHouseLevel.

1
2
3
4
5
6
7
8
/**
 * Create text, buttons, indicators, etc
 */
override protected function createGUI():void {
    var instructions:FlxText = new FlxText(0, 0, levelSize.x, "Use ARROW keys to walk around");
    instructions.alignment = "center";
    guiGroup.add(instructions);
}

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


Теперь у вас есть все необходимое для создания собственных уровней RPG. Спасибо, что остались со мной и пошли поиграть!