Реклама
Перетяжка // Коробка 3.0

Создание кроссплатформенной игры на Flutter за неделю

Рассказываем, как сделать мобильный кликер и адаптировать его для Android, iOS, ПК и веба.

Обложка: Создание кроссплатформенной игры на Flutter за неделю

Часто мобильные разработчики не хотят браться за создание игр, потому что это кажется долгим и трудоёмким процессом. Это работа с памятью и спрайтами, сложные UI/UX и много оптимизации. И если вы уже разрабатывали приложения под Android, то наверняка представляете, как было бы трудно создавать игры на нативном коде. Именно по этой причине стоить задуматься о разработке на Flutter — SDK от Google для создания кроссплатформенных приложений.

На GitHub есть пример игры Spaceblast, созданной на этом SDK.

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

Разработка игры

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

			GestureDetector(
 onTapDown: (TapDownDetails details) => damage(details),
)
var _bossDamage = 980;

void damage(TapDownDetails details) {
 setState(() {
   _bossDamage = _bossDamage - 30 <= 0 ? 980 : _bossDamage - 30;
 });
}
		

Вот что должно получиться после добавления визуальных элементов:

Создание кроссплатформенной игры на Flutter за неделю_7

Следующим шагом будет добавление UI-элементов, которые сделают игру более приятной. Потребуются спрайты персонажа, боссов (которым будет наноситься урон), бонусов и монет.

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

			String hero() {
 return tap ? "assets/character/attack.png" : "assets/character/idle.png";
}
		

Для боссов же понадобится больше изображений. Это сделает игру более разнообразной и интересной.

			static List getBosses() {
 var list = List();
 list.add(Bosses("Lunabi", 450, "assets/boss/boss_one.png"));
 list.add(Bosses("ivygrass", 880, "assets/boss/boss_two.png"));
 list.add(Bosses("Tombster", 1120, "assets/boss/boss_three.png"));
 list.add(Bosses("Glidestone", 2260, "assets/boss/boss_four.png"));
 list.add(Bosses("Smocka", 2900, "assets/boss/boss_five.png"));
 list.add(Bosses("Clowntorch", 4100, "assets/boss/boss_six.png"));
 list.add(Bosses("Marsattack", 5380, "assets/boss/boss_seven.png"));
 list.add(Bosses("Unknown", 7000, "assets/boss/boss_eight.png"));
 list.add(Bosses("ExArthur", 10000, "assets/boss/boss_nine.png"));
 return list;
}
		

У каждого босса есть имя, показатель жизни и спрайт.

Для бонусов код аналогичный:

			static List getPowerUps() {
 var list = List();
 list.add(PowerUps("Master Sword", 2.15, false, 50));
 list.add(PowerUps("Lengendary Sword", 2.45, false, 180));
 list.add(PowerUps("Keyblade", 3.75, false, 300));
 list.add(PowerUps("Lightsaber", 4.95, false, 520));
 list.add(PowerUps("Buster Sword", 6.15, false, 1700));
 list.add(PowerUps("Soul Edge", 8.65, false, 2400));
 return list;
}
		

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

После этого можно добавить визуальный «хитбокс», который будет отображаться при каждом нажатии на экран:

			Widget hitBox() {
 if (tap) {
   return Positioned(
     top: yAxis,
     left: xAxis,
     child: Column(
       children: [
         Padding(
           padding: const EdgeInsets.only(bottom: 20.0),
           child: Material(
             color: Colors.transparent,
             child: StrokeText(
               "-${damageUser.toInt().toString()}",
               fontSize: 14.0,
               fontFamily: "Gameplay",
               color: Colors.red,
               strokeColor: Colors.black,
               strokeWidth: 1.0,
             ),
           ),
         ),
         Image.asset(
           "assets/elements/hit.png",
           fit: BoxFit.fill,
           height: 80.0,
           width: 80.0,
         ),
       ],
     ),
   );
 } else {
   return Container();
 }
}
		

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

При прикосновении используется параметр TapDownDetails для получения X и Y координат пальца.

Ещё нужно добавить логику скрытия этих элементов после того, как игрок отведёт палец от экрана.

			GestureDetector(
 onTapDown: (TapDownDetails details) => damage(details),
 onTapUp: (TapUpDetails details) => hide(null),
 onTapCancel: () => hide(null),
),
		

В добавлении списка бонусов тоже нет ничего сложного:

			ListView.builder(
 padding: EdgeInsets.only(bottom: 20.0, left: 10.0, right: 10.0),
 itemCount: list.length,
 itemBuilder: (context, position) {
   PowerUps powerUp = list[position];
   int bgColor = !powerUp.bought && coins >= powerUp.coins
       ? 0xFF808080
       : !powerUp.bought ? 0xFF505050 : 0xFF202020;

   return swordElement(bgColor, powerUp, position);
 },
)
		

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

			Widget swordElement(int bgColor, PowerUps powerUp, int position) {
 return Padding(
   padding: const EdgeInsets.symmetric(
     vertical: 5.0,
   ),
   child: Container(
     height: 70,
     child: Card(
       color: Color(bgColor),
       child: Row(
         children: [
           Expanded(
             child: Padding(
               padding: const EdgeInsets.symmetric(horizontal: 20.0),
               child: Text(
                 powerUp.name,
                 style: Utils.textStyle(11.0),
               ),
             ),
           ),
           Padding(
             padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 20.0),
             child: FancyButton(
               size: 20,
               child: Row(
                 children: [
                   Padding(
                     padding: const EdgeInsets.only(left: 10.0, bottom: 2, top: 2),
                     child: Text(
                       !powerUp.bought ? "BUY" : "BOUGHT",
                       style:
                       Utils.textStyle(13.0, color: !powerUp.bought ? Colors.white : Colors.grey),
                     ),
                   ),
                   Padding(
                     padding: EdgeInsets.only(left: 8.0, right: !powerUp.bought ? 2.0 : 0.0),
                     child: Text(
                       !powerUp.bought ? powerUp.coins.toString() : "",
                       style: Utils.textStyle(13.0),
                     ),
                   ),
                   coinVisibility(powerUp.bought),
                 ],
               ),
               color: !powerUp.bought && coins >= powerUp.coins
                   ? Colors.deepPurpleAccent
                   : Colors.deepPurple,
               onPressed: !powerUp.bought && coins >= powerUp.coins ? () => buyPowerUp(position) : null,
             ),
           )
         ],
       ),
     ),
   ),
 );
}
		

На текущем моменте игра выглядит так:

Создание кроссплатформенной игры на Flutter за неделю_28

Звуки и музыка

Звуки играют очень важную роль в играх. Добавим их:

			AudioPlayer hitPlayer;
AudioCache hitCache;
hitPlayer = AudioPlayer();
hitCache = AudioCache(fixedPlayer: hitPlayer);
// Если аудио ещё проигрывается, то его нужно остановить и проиграть заново
hitPlayer.pause();
hitCache.play('audio/sword.mp3');
		

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

			AudioCache musicCache;
AudioPlayer instance;
void playMusic() async {
 musicCache = AudioCache(prefix: "audio/");
 instance = await musicCache.loop("bgmusic.mp3");
}
		

Звук будет проигрываться из InitState и будет остановлен из метода Dispose. Если присмотреться, то в игре можно увидеть кастомные шрифты — они не обязательны, но делают игру более ламповой.

Экран приветствия

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

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

Ещё в экран приветствия был добавлен эффект Blur (размытие) и немного частиц огня и пепла. Код эффекта выглядит так:

			BackdropFilter(
 filter: ImageFilter.blur(
   sigmaX: 4.0,
   sigmaY: 4.0,
 ),
 child: Align(
   alignment: Alignment.bottomCenter,
   child: Align(
     alignment: Alignment.topCenter,
     heightFactor: heroYAxis,
     child: Image.asset(
       heroAsset(),
       width: size / 1.5,
       height: size / 1.5,
       fit: BoxFit.cover,
     ),
   ),
 ),
)
		

Код частиц можно найти в этом репозитории.

Вот так выглядит итоговый результат:

Создание кроссплатформенной игры на Flutter за неделю_42

Добавление механик в игру

По желанию можно усложнить игру, сделав её разнообразнее и интереснее. Например, можно добавить ограничение по времени. Оставшееся время будет увеличиваться при каждом убийстве босса. Ещё можно дать возможность поделиться результатом в соц. сетях.

Создание кроссплатформенной игры на Flutter за неделю_45

Для отсчёта времени используется обычный AnimationController. Он отсчитывает прошедшее время в миллисекундах, пока не достигнет 0. Если вам нужно форматировать время для строки, то используйте этот фрагмент кода:

			String get timerString {
 Duration duration = controller.duration * controller.value;
 return '$ {(duration.inMinutes) .toString (). padLeft (2,' 0 ')}: $ {(duration.inSeconds% 60) .toString (). padLeft (2,' 0 ')}';
}
		

Если счётчик достигнет 0, то будет активироваться «Game over»:

			controller.addStatusListener ((status) {
 if (status == AnimationStatus.dismissed) {
   setState (() {
     gameOver = true ;
   });
 }
});
		

Поддержка геймпада

Это экспериментальная функция, поэтому с ней может происходить всё, что угодно.

Пока можно попробовать варианты Android + Switch Pro Controller (PS4 и Xbox Controller остаются в стороне).

Для работы с геймпадом нужно использовать каналы. Вначале код для Android Kotlin:

			var channel =  MethodChannel(flutterView, "gamepad")
channel.setMethodCallHandler { call, result ->
   when {
       call.method == "isGamepadConnected" -> {
         val ids = InputDevice.getDeviceIds()
         for (id in ids) {
           val device = InputDevice.getDevice(id)
           val sources = device.sources

           if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) {
             result.success(true)
           }
         }
         result.success(false)
       }
       call.method == "getGamePadName" -> {
         val gamepadIds = InputDevice.getDeviceIds()
         for (id in gamepadIds) {
           val device = InputDevice.getDevice(id)
           val sources = device.sources

           if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) {
             result.success(device.name)
           }
         }
       }
       else -> result.notImplemented()
   }
 }
}
		

Его можно разместить внутри класса MainActivity. Ещё вы можете реализовать интерфейс InputManager.InputDeviceListner для обнаружения подключения/отключения геймпада:

			var inputManager = getSystemService(Context.INPUT_SERVICE) as InputManager
override fun onResume() {
 super.onResume()
 inputManager.registerInputDeviceListener(this, null)
}

override fun onPause() {
 super.onPause()
 inputManager.unregisterInputDeviceListener(this)
}

override fun onInputDeviceRemoved(deviceId: Int) {
 channel.invokeMethod("gamepadRemoved", true)
}

override fun onInputDeviceAdded(deviceId: Int) {
 channel.invokeMethod("gamepadName", InputDevice.getDevice(deviceId).name)
}

override fun onInputDeviceChanged(deviceId: Int) {
 channel.invokeMethod("gamepadName", InputDevice.getDevice(deviceId).name)
}
		

При каждом из этих событий Android будет отправлять данные в канал:

			static const MethodChannel _channel = const MethodChannel('gamepad');

static Future get isGamePadConnected async {
 final bool isConnected = await _channel.invokeMethod('isGamepadConnected');
 return isConnected;
}

static Future get gamepadName async {
 final String name = await _channel.invokeMethod("getGamePadName");
 return name;
}
		

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

			_channel.setMethodCallHandler((call) async {
 switch (call.method) {
   case "gamepadName":
     setState(() {
       _gamepadName = call.arguments;
     });
     break;
   case "gamepadRemoved":
     setState(() {
       _gamepadName = "Undefined";
     });
     break;
 }
});
		

Аргументы могут быть следующими:

Создание кроссплатформенной игры на Flutter за неделю_62

Результат:

Создание кроссплатформенной игры на Flutter за неделю_64

Запуск на других платформах

Наверное, главным преимущество Flutter является его кроссплатформенность. Игры на нём можно запускать на Android, iOS, ПК и даже в вебе.

iOS

Первым делом вам потребуется Mac с установленным на нём XCode. Только так вы сможете собрать билд игры.

Прим. перев. Есть также вариант использовать codemagic.io.

Создание кроссплатформенной игры на Flutter за неделю_70

Если вы используете Android Studio, то после нажатия на RUN выберете iOS устройство. Для этого у вас должна быть включена опция «Open iOS Simulator».

Ещё вы можете запустить игру на определённом устройстве по его ID. Идентификатор устройства можно посмотреть во «flutter devices», а после выполнить следующее:

			flutter run -d {device id}
// ID указывать без фигурных скобок
		

ПК

Для компьютеров сборка игры чуть сложнее из-за дополнительных шагов. Её можно проводить не только на Mac, но и на Windows или Linux.

Процесс сборки игры детально описан в этой статье. В терминале нужно выполнить следующую команду:

export ENABLE_FLUTTER_DESKTOP=true

После этого вы сможете клонировать официальный репозиторий Flutter для ПК:

git clone https://github.com/google/flutter-desktop-embedding.git

cd example

Теперь вы должны скопировать папки «Mac», «Windows» и «Linux» в папку вашего проекта.

После этого нужно внести небольшие изменения в код:

			void _setTargetPlatformForDesktop() {
 TargetPlatform targetPlatform;
 if (Platform.isMacOS) {
   targetPlatform = TargetPlatform.iOS;
 } else if (Platform.isLinux || Platform.isWindows) {
   targetPlatform = TargetPlatform.android;
 }
 if (targetPlatform != null) {
   debugDefaultTargetPlatformOverride = targetPlatform;
 }
}
		

Этот метод нужно вызвать в main().

			void main() {
 _setTargetPlatformForDesktop();
 runApp(TapHero());
}
		

Убедитесь, что вы импортировали эти два класса:

			import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride;
		

Примечание Если вам нужно заблокировать поворот экрана на телефоне, то добавьте этот код перед возвратом в методе сборки:

			SystemChrome.setPreferredOrientations([
 DeviceOrientation.portraitUp,
]);

import 'package:flutter/services.dart';
		

И наконец игру можно запускать на ПК. Целевая платформа должна появится в списке устройств.

Создание кроссплатформенной игры на Flutter за неделю_91

Веб

Да, браузеры тоже никто не отменял. О сборке игр на Flutter под веб-версию описано в этой статье.

Процесс идентичен сборке под ПК. Сначала вам нужно клонировать репозиторий (там есть пример «hello_world»):

git clone git@github.com:flutter/flutter_web.git

cd examples/hello_world

Убедитесь, что вы используете последнюю версию Flutter:

flutter upgrade

И ещё вам потребуется пакет для веб-разработки:

flutter packages pub global activate webdev

flutter packages upgrade

Именно благодаря ему вы сможете собирать игры и запускать их в браузере. После этого нужно указать путь к Dart и Webdev и изменить импортируемые библиотеки для flutter_web, например:

			import 'package:flutter_web/material.dart';
		

После этого нужно перенести все ассеты игры в папку /web/assets. Под конец остаётся нажать на строку webdev serve над терминалом.

Вот и всё. Теперь открыть игру в браузере можно по ссылке http://localhost:8080/.

Создание кроссплатформенной игры на Flutter за неделю_106

Получение фактического URL

Знаете ли вы что-то о хостинге на Firebase? С его помощью вы сможете разместить ваше приложение и получить его URL. Вам нужна услуга «хостинг» и подробный гайд по ней:

После этого вам нужно будет создать своё Flutter Web App:

webdev build

В вашем проекте вы увидите новые папки: «build» и «public». Скопируйте всё содержимое первой во вторую. Последним шагом будет деплой:

firebase deploy

Вот и всё! Теперь ваше приложение должно быть доступно по адресу .web.app вашего домена. В игру из примера можно поиграть тут: https://gametaphero.web.app/#/

Примечание В папке public есть файл index.html. Он следует той же логике, что и любая веб-страница: для сайта можно установить иконку, цвет строки поиска (для Chrome mobile), название приложения и т. д.

Рекомендуем