Хотя данная статься имеет больше актуальности для мобильных устройств, для компьютеров оптимизация так же ощущается довольно заметно
Больше года я разрабатываю свою игру на движке Flame, который написан на языке dart и полностью интегрирован во Flutter. Хочу показать вам пару картинок, как сейчас всё это выглядит.
![](https://habrastorage.org/getpro/habr/upload_files/e25/24e/ecd/e2524eecd5dd36c61f60829344eeca9d.png)
![](https://habrastorage.org/getpro/habr/upload_files/10f/996/778/10f9967789206e7725fc3ba9057ae5e3.png)
Всё это сделано стандартным набором программ: Tiled и движок Flame.
Вот проблемы, которые вы получите при работе с данными компонентами gameDev’а, и дальше — пути их решения. (Исходники положу в самом низу)
Tiled
Свободная программа для создания тайловых карт. Также здесь можно создавать слои препятствий с любыми свойствами. Я думаю, она всем известна — супер удобная, претензий к ней нет.
Карта на данный момент у меня такая: 297*297 тайлов. Каждый тайл — это квадрат 32*32 пикселя, т. е. моя карта 9504 пикселя на 9504. Плюс к этому она содержит 14 слоёв тайлов и 4 слоя объектов. И вот тут начинаются проблемы с отображением этого всего.
Вообще сделаем такую ремарку: во Flame есть чтение проектов Tiled прямо из коробки. Проблема в том, что сам класс, который создаётся после прочтения проекта, получается слишком тяжёлым. Из-за этого даже проект 30*30 тайлов читается около 300 миллисекунд.
Поэтому работать напрямую с проектами, как говорится в примерах на сайте Flame, очень невыгодно. т. е. нельзя просто сделать в Tiled много разных нумерованных карт и загружать их прямо во время рендеринга, когда вы подбегаете близко к краю экрана. Игра попросту зафризится на полсекунды и вы, конечно, это увидите. Я проверял это, и даже при двух слоях тайлов наблюдается такая картина. Играть так долго точно никто не захочет и через пять минут просто выключит постоянно фризищуюся поделку.
Вторая проблема после очень долгого чтения *.tmx это то, что удобно рисовать большую карту именно на большом полотне в Tiled. А разделить в программе Tiled огромную карту на части нельзя. И даже если мы разделим — не решим проблему долгой загрузки.
Можно отрендерить её в PNG прямо из программы Tiled и загружать обычным Flame.images.load('name'), но тут есть один нюанс: такая большая карта, как 9500 на 9500, будет жрать практически все FPS на андроиде при отображении. Flame очень плохо переносит большого размера PositionComponent.
Сейчас покажу, насколько удобно работать именно на большом холсте. Это приближенный кусок:
![](https://habrastorage.org/getpro/habr/upload_files/685/16a/8ee/68516a8ee5a8434e521afad824b27203.png)
А вот это вся карта целиком (первая картинка вон там сверху слева):
![](https://habrastorage.org/getpro/habr/upload_files/7c3/654/5ba/7c36545ba592a07dd30e3548719e9203.png)
Вот при таком масштабе сразу понятно, что и где должно находиться в твоей игре и можешь сделать большой и интересный мир.
Решения проблем загрузки игровой карты и оптимизация отображения в Tiled
Нам придётся сделать отдельный модуль для прекомпиляции всей карты из файла *.tmx в множество мелких файлов, разбитых по столбцам и ячейкам.
Я наткнулся здесь на статью по оптимизации отображения тайлов во Flame. Это не решает проблему долгой загрузки карт, но даёт отправную точку классом ImageBatchCompiler из пакета flame_tiled_utils (https://pub.dev/packages/flame_tiled_utils).
Исходя из того, что на ходу читать *.tmx очень затратно, нам приходится заранее скомпилировать её в простые составные, чтобы на ходу их читать. Тем более всё равно придётся дробить такую большую картину карты на ячейки. В итоге мы с помощью доработок ImageBatchCompiler прекомпилируем карту в PNG размером ваших ячеек с названиями ваших столбцов и ячеек. Это первый шаг. Плюс к этому я разделил названиями классы тайлов на те, что над игроком и на те что под ним:
![](https://habrastorage.org/getpro/habr/upload_files/c6d/107/a59/c6d107a598fd932a5ba1aaf4ff804254.png)
Вот так я выделил то, что будет над игроком. В итоге у меня есть две картинки на ячейку: ту, что под игроком:
![](https://habrastorage.org/getpro/habr/upload_files/37d/05b/3fb/37d05b3fb60cbadde29ad2119c551b3f.png)
И ту, что над:
![](https://habrastorage.org/getpro/habr/upload_files/bbd/8fd/7da/bbd8fd7da61f1c7b6f0cf76616f06a9f.png)
Мы будем создавать их обычным Sprite и для нижних ставить priority = 0, а для верхних картинок — priority = max
В итоге у нас уже получается вкусная картинка:
![](https://habrastorage.org/getpro/habr/upload_files/053/3d7/e20/0533d7e20ea280d5b4cffe6632446e8d.png)
![](https://habrastorage.org/getpro/habr/upload_files/b52/505/1a8/b525051a8ff4bfc95fe295296a177c51.png)
Следует добавить, что вам надо добавить пустую картинку в проект такого же размера и добавлять её в каждую ячейку во время прекомпиляции PNG. При компиляции картинки она обрезается до минимально возможных размеров, и если у вас допустим только одна верхушка дерева, то получится картинка 32*32, а куда её потом помещать на игровую карты будет совсем не ясно.
Теперь надо разобраться с анимированными тайлами. Они прекомпилируются так же в текстовые файлы: файлы НАД игроком и файлы ПОД ним. Я сделал свой собственный тип файлов *_high.anim и *_down.anim. Внутри файлов — обычный xml, который содержит путь к изображению, высоту/ширину тайла, длительность каждой анимации и список ее позиций в ячейке, если там много одинаковых анимаций. При игре мы будем создавать SpriteAnimationComponent на основе этих файлов. Как ни странно - создание SpriteAnimationComponent не расходует сильно ресурсы и создаются они моментально
Теперь осталось разобраться со слоями объектов в Tiled. Я подхожу здесь точно так же: поочерёдно прохожу по всем ячейкам и записываю все линии, которые входят в эту ячейку, если линия выходит за ячейку — нахожу пересечение и записываю эту точку как нужную. Таким образом я получаю краткий список всех объектов, включая все их свойства и прочее в моём формате файлов *.objXml. Эти файлы я буду читать на ходу. Они содержат начала координат на карте Tiled и все дальнейшие точки по порядку относительно начала. Они будут превращаться в мой собственный новый класс столкновений, который хранит просто лист точек и возможность поворачиваться, умножаться на вектор и отдавать правильные точки на игровой карте в зависимости от того - может ли этот объект менять своё местоположение. Как правило - бОльшая часть (намного бОльшая) объектов столкновений в игре - статично. Поэтому нет смысла держать класс встроенный PolygonHitbox, который и обновляется каждый тик, и хранит в себе кучу проверок и так далее. Нам просто надо знать список точек по порядку, по которым мы будем строить линии когда мы окажемся игроком в близости от этого объекта.
Flame
Обсчёт столкновений — самое важное и ответственное, что есть в игре. Это написано и в упомянутой выше статье.
Во‑первых, все классы у Flame довольно громоздкие. Не обходит это и класс столкновений (ShapeHitbox). Там и ретрейсинг сразу вшит, и прочее, прочее.
Во‑вторых, во Flame НЕЛЬЗЯ создавать одну линию как объект для столкновения, можно толькр выпуклые объекты, т.е минимум полигон из трёх точек.
В‑третьих, нет суммарного вызова на определённый тип предметов, т. е., попав в узкий переулок, вы будете получать сигналы от игры сначала о столкновении слева, потом справа. Всё это приводит к тому, что пользоваться такими ограничениями не имеет смысла.
В четвёртых: по какой-то причине вам дают не те точки, которые находятся внутри вашего тела, а просто точку пересечения с вашей линией. Из этого следует - что основываясь на этой информации вы не можете вообще ничего сделать, кроме как сказать что да - мы сейчас с чем-то столкнулись. Эта информация не несёт вообще никакого смысла, если нам надо отработать поведение столкновения. Должны передаваться точки, которые находятся внутри моего объекта, чтобы он сменил позиции и перестал включать в себя препятствие, чтобы оно было на границе моего хитбокса.
Минусы от невозможности создать такие вещи — все заборы или линии берега приходится делать кубиками, что увеличивает количество точек и линий столкновений в два раза! И ровно во столько же раз увеличивается обсчёт на возможность столкновений. Невозможность создать НЕвыпуклые многоугольники также заставляет дробить закутки и дома с одним открытым входом и прочие комнаты, помещения и т.д. и т.п. Если вы начнёту заходить в вершину треугольника - вы получите просто две точки на границе вас, а не ту вершину, которая сейчас внутри вашего хитбокса. Вот картинка:
![](https://habrastorage.org/getpro/habr/upload_files/269/2f1/6cb/2692f16cb20fe1759027a33579f84fb0.png)
Желтые точки это то, что даст нам Flame из коробки. Зелёная точка - эта точка, которая нам нужна, которая при перемещении должна оказаться вне нашего тела. Фиолетавая линия - нахождение пересечения центра, нашей точки и любой грани нашего тела. Чёрная стрелка - нормаль этой грани, по которой мы должны двигаться. Я даю в своём механизме фиолетовые точки (середина от зелёной и жёлтой), потому что при нахождении к границам объекта иногда непонятно куда двигаться, либо влево, либо вниз и персонаж застревает у края стены. Фиолетовые точки сглаживают это и таких склеек нет.
![Желтые - что даст из коробки Flame. Красная - что даю я Желтые - что даст из коробки Flame. Красная - что даю я](https://habrastorage.org/getpro/habr/upload_files/8bd/cc8/53e/8bdcc853e4a95d4eed9b17ff43613b40.png)
В итоге я полностью удалил из игры встроенный механизм и создал свой класс объектов, который просто содержит список точек по порядку. А в механизм обсчёта я добавил вначале сложение для каждого двигающегося объекта всех столкновений с препятствиями, а потом уже вызов его колбэка с совокупностью этих точек. Поэтому если вы ударяетесь в данный момент и справа, и слева — вы можете понять, что варианта выхода из этой ситуации два: либо вверх, либо вниз.
Дополнительный плюс делить объекты по ячейкам - отсекать ненужные проверки на пересечения если препятствие принадлежит другой ячейке.
В итоге я пришёл к такому механизму, что у меня есть два принципиально разных класса: объект столкновения и любой другой триггер — меч противника и прочее, что не нуждается во всех точках соприкосновения и должно вызывать коллбэк сразу. Точки объектов сначала добавляются в массив, только потом обрабатываются, столкновения с другими предметами же обрабатывается сразу.
Итоги
Tiled — отличный механизм для создания огромных и интерактивных карт. Но приходится всё это прекомпилировать. Это недолго и нестрашно, но от этого никуда не деться, при более-менее большой карте вам надо оптимизировать прогрузку *.tmx. Да и честно — это делается пару раз, и карта особо часто не меняется, уж тем более после того, как попадёт в google play.
Flame — отличный движок с точки зрения отображения и создания, а также простоты работы с анимированными компонентами, так как есть единая система взаимодействия компонентов, камеры, кэша файлов и создания HUD прямо стандартными классами Flutter (class Joystick extends StatelessWidget => return Center(child: joystick())). Минусы — всё, что связано с обработкой коллизий объектов. К сожалению, проще самому переписать полностью весь механизм, но поверьте — это всё супер несложно.
А что там у меня в игре
После прекомпиляции у меня 2699 файлов общим весом 20 мегабайт.
Из-за того, что нельзя получить доступ к asset'ам из другого потока на android, мне приходится при саааааамом первом запуске игры копировать *.anim и *.objXml во внутреннюю память телефона, чтобы при следующих запусках двумя другими потоками читать это и передавать в главный поток для кэша. В итоге загрузка всех этих файлов происходит за 3 секунды. Потом начинается игра, и мы видим 9 прямоугольников вокруг игрока, которые бесшовно прогружаются по ходу изменения позиции. Каждый прямоугольник 352*288 пикселей. На телефоне FPS в самых тяжёлых моментах не опускается ниже 40 FPS, а так постоянно 60. Нет никаких глазу заметных прифризов и прочего при передвижении или столкновениях.
Технические возможности
Можно создавать и крутить/увеличивать ЛЮБЫЕ объекты вообще и столкновения будут определяться хорошо классом MapObstacle или наследуя от него. Столкновения все складываются, и только потом отдаётся команда на обработку. В обработчик входят только точки, которые находятся внутри вашего персонажа. Т.е. логика работы со столкновениями предполагает, что вы будете менять позицию так, чтобы эта точка оказалась на границе вашего двигающегося тела.
Можно рисовать любую вообще кару, не большее 9600*9600 пикселей в Tiled, потому что больше просто не даст загрузить сам Flame. После создания карты вы должны прекомпилировать её на минифайлы по размерам вашей ячейки. И всё, потом только при смене позиции персонажа менять ячейки.
На данные момент есть баг в прекомпиляции объектов - иногда появляются дыра при дроблении объекта из карты *.tmx в ObjXml. Но задача здесь чисто аналитическая, и решается. Может выложу решение когда доберусь до него:
![](https://habrastorage.org/getpro/habr/upload_files/292/73d/2d9/29273d2d9985f12e3fd0fe189bd56c6d.png)
Исходники
Вспомогательные классы
class LoadedColumnRow
{
LoadedColumnRow(this.column, this.row);
int column;
int row;
@override
bool operator ==(Object other) {
return other is LoadedColumnRow && other.column == column && other.row == row;
}
@override
int get hashCode => column.hashCode ^ row.hashCode;
}
Vector2 f_pointOfIntersect(Vector2 a1, Vector2 a2, Vector2 b1, Vector2 b2)
{
double s1_x, s1_y, s2_x, s2_y;
s1_x = a2.x - a1.x;
s1_y = a2.y - a1.y;
s2_x = b2.x - b1.x;
s2_y = b2.y - b1.y;
double s, t;
s = (-s1_y * (a1.x - b1.x) + s1_x * (a1.y - b1.y)) /
(-s2_x * s1_y + s1_x * s2_y);
t = (s2_x * (a1.y - b1.y) - s2_y * (a1.x - b1.x)) /
(-s2_x * s1_y + s1_x * s2_y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
return Vector2(a1.x + (t * s1_x), a1.y + (t * s1_y));
}
return Vector2.zero();
}
class PointCust extends PositionComponent
{
PointCust({required super.position,this.color});
final ShapeHitbox hitbox = CircleHitbox();
Color? color;
@override
void onLoad()
{
priority = 800;
size = Vector2(5, 5);
hitbox.paint.color = color ?? BasicPalette.green.color;
hitbox.renderShape = true;
add(hitbox);
Future.delayed(const Duration(milliseconds: 30),(){
removeFromParent();
});
}
}
Класс компилятора изображений и анимаций
import 'dart:io';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
import 'package:flame_tiled/flame_tiled.dart';
import 'package:flame_tiled_utils/flame_tiled_utils.dart';
import 'package:game_flame/components/physic_vals.dart';
enum RenderCompileMode{
Background,
Foreground,
All
}
Future processTileType(
{required RenderableTiledMap tileMap,
required TileProcessorFunc addTiles,
required List<String> layersToLoad,
required RenderCompileMode renderMode,
bool clear = true}) async
{
for (final layer in layersToLoad) {
final tileLayer = tileMap.getLayer<TileLayer>(layer);
final tileData = tileLayer?.data;
if (tileData != null) {
int xOffset = 0;
int yOffset = 0;
for (var tileId in tileData) {
bool isNeedAdd = true;
if (tileId != 0) {
final tileset = tileMap.map.tilesetByTileGId(tileId);
final firstGid = tileset.firstGid;
if (firstGid != null) {
tileId = tileId - firstGid; //+ 1;
}
final tileData = tileset.tiles[tileId];
if(renderMode == RenderCompileMode.Background && (tileData.class_ == 'high' || tileLayer!.name.startsWith('xx'))) {
isNeedAdd = false;
}
if(renderMode == RenderCompileMode.Foreground){
if(tileData.class_ != 'high' && !tileLayer!.name.startsWith('xx')) {
isNeedAdd = false;
}
}
if(isNeedAdd){
final position = Vector2(xOffset.toDouble() * tileMap.map.tileWidth,
yOffset.toDouble() * tileMap.map.tileWidth);
final tileProcessor = TileProcessor(tileData, tileset);
await addTiles(
tileProcessor,
position,
Vector2(tileMap.map.tileWidth.toDouble(),
tileMap.map.tileWidth.toDouble()));
}
}
xOffset++;
if (xOffset == tileLayer?.width) {
xOffset = 0;
yOffset++;
}
}
}
}
if (clear) {
tileMap.map.layers
.removeWhere((element) => layersToLoad.contains(element.name));
for (var rl in tileMap.renderableLayers) {
rl.refreshCache();
}
}
}
class IntPoint
{
IntPoint(this.x, this.y);
int x; //column
int y; //row
@override
bool operator ==(other)
{
if(other is IntPoint){
if(x != other.x || y != other.y){
return false;
}
return true;
}else{
return false;
}
}
@override
int get hashCode => x.hashCode + y.hashCode;
}
class AnimationPos
{
String sourceImg = '';
final List<IntPoint> spritePos = [];
final List<double> stepTimes = [];
int width = 0;
int height = 0;
@override
operator ==(other)
{
if(other is AnimationPos){
if(spritePos.length == other.spritePos.length){
for(int i=0;i<spritePos.length;i++){
if(spritePos[i] != other.spritePos[i]){
return false;
}
}
}else{
return false;
}
if(stepTimes.length == other.stepTimes.length){
for(int i=0;i<stepTimes.length;i++){
if(stepTimes[i] != other.stepTimes[i]){
return false;
}
}
}else{
return false;
}
if(sourceImg != other.sourceImg){
return false;
}
}else{
return false;
}
return true;
}
@override
int get hashCode => sourceImg.hashCode + spritePos[0].hashCode + stepTimes[0].hashCode;
}
class MySuperAnimCompiler { //Доработанный ImageBatchCompiler
List<Map<Sprite?, List<Vector2>>> _mapsSprite = [];
Map<Sprite?, List<Vector2>> _allSpriteMap = {};
Map<AnimationPos, List<Vector2>> _animations = {};
Future addTile(Vector2 position, TileProcessor tileProcessor) async
{
var animation = await tileProcessor.getSpriteAnimation();
if (animation == null) {
var sprite = await tileProcessor.getSprite();
_allSpriteMap.putIfAbsent(sprite, () => []);
_allSpriteMap[sprite]!.add(position);
} else {
AnimationPos pos = AnimationPos();
pos.sourceImg = tileProcessor.tileset.image!.source!;
pos.width = tileProcessor.tileset.tileWidth!;
pos.height = tileProcessor.tileset.tileHeight!;
Image image = await Flame.images.load(pos.sourceImg);
int maxColumn = image.width ~/ pos.width;
for (final frame in tileProcessor.tile.animation) {
pos.stepTimes.add(frame.duration / 1000);
pos.spritePos.add(
IntPoint(frame.tileId % maxColumn, frame.tileId ~/ maxColumn));
}
_animations.putIfAbsent(pos, () => []);
_animations[pos]!.add(position);
}
}
void addLayer() {
_mapsSprite.add(_allSpriteMap);
_allSpriteMap = {};
}
Future<void> compile(String path) async
{
print('start compile! $path');
final nullImage = await Flame.images.load('null_image-352px.png');
for (int cols = 0; cols < GameConsts.maxColumn; cols++) {
for (int rows = 0; rows < GameConsts.maxRow; rows++) {
bool isWas = false;
var position = Vector2(cols * GameConsts.lengthOfTileSquare.x,
rows * GameConsts.lengthOfTileSquare.y);
Rectangle rec = Rectangle.fromPoints(position, Vector2(
position.x + GameConsts.lengthOfTileSquare.x,
position.y + GameConsts.lengthOfTileSquare.y));
final composition = ImageCompositionExt();
for (int i = 0; i < _mapsSprite.length; i++) {
var currentSprites = _mapsSprite[i];
for (final spr in currentSprites.keys) {
if (spr == null) {
continue;
}
for (final pos in currentSprites[spr]!) {
if (!rec.containsPoint(pos + Vector2.all(1))) {
continue;
}
composition.add(spr.image, pos - position, source: spr.src);
isWas = true;
}
}
}
if (isWas) {
composition.add(
nullImage, Vector2.all(0), source: nullImage.getBoundingRect());
final composedImage = composition.compose();
var byteData = await composedImage.toByteData(
format: ImageByteFormat.png);
File file = File('assets/metaData/$cols-${rows}_$path.png');
file.writeAsBytesSync(byteData!.buffer.asUint8List());
}
}
}
for (int cols = 0; cols < GameConsts.maxColumn; cols++) {
for (int rows = 0; rows < GameConsts.maxRow; rows++) {
var position = Vector2(cols * GameConsts.lengthOfTileSquare.x,
rows * GameConsts.lengthOfTileSquare.y);
Rectangle rec = Rectangle.fromPoints(position, Vector2(
position.x + GameConsts.lengthOfTileSquare.x,
position.y + GameConsts.lengthOfTileSquare.y));
bool isStartFile = false;
for (final anim in _animations.keys) {
String animText = '';
List<Vector2> currentPoints = _animations[anim]!;
for (final point in currentPoints) {
if (!rec.containsPoint(point + Vector2.all(1))) {
continue;
}
if (animText == '') {
animText = '<an src="${anim.sourceImg}" w="${anim.width}" h="${anim.height}" >\n';
for (int i = 0; i < anim.stepTimes.length; i++) {
animText +=
'<fr dr="${anim.stepTimes[i]}" cl="${anim.spritePos[i]
.x}" rw="${anim.spritePos[i].y}"/>\n';
}
}
animText +=
'<ps x="${point.x}" y="${point.y}"/>\n';
}
if (animText != '') {
File file = File('assets/metaData/$cols-${rows}_$path.anim');
if(!isStartFile){
isStartFile = true;
file.writeAsStringSync('<p>\n', mode: FileMode.append);
}
file.writeAsStringSync(animText, mode: FileMode.append);
file.writeAsStringSync('</an>\n', mode: FileMode.append);
}
}
if (isStartFile) {
File file = File('assets/metaData/$cols-${rows}_$path.anim');
file.writeAsStringSync('</p>\n', mode: FileMode.append);
}
}
}
}
}
Класс столкновений
abstract class DCollisionEntity extends Component
{
List<Vector2> _vertices;
DCollisionType collisionType;
bool isSolid;
bool isStatic;
bool isLoop;
double angle = 0;
Vector2 scale = Vector2(1, 1);
Vector2 _center = Vector2(0, 0);
Set<Vector2> obstacleIntersects = {};
LoadedColumnRow? _myCoords;
KyrgyzGame game;
int? column;
int? row;
double width = 0;
double height = 0;
bool onlyForPlayer = false;
Vector2 _minCoords = Vector2(0, 0);
Vector2 _maxCoords = Vector2(0, 0);
Vector2? transformPoint;
List<Vector2> get vertices => _vertices;
DCollisionEntity(this._vertices,
{required this.collisionType, required this.isSolid, required this.isStatic
, required this.isLoop, required this.game, this.column, this.row, this.transformPoint})
{
if (isStatic) {
int currCol = column ?? vertices[0].x ~/ GameConsts.lengthOfTileSquare.x;
int currRow = row ?? vertices[0].y ~/ GameConsts.lengthOfTileSquare.y;
_myCoords = LoadedColumnRow(currCol, currRow);
game.gameMap.collisionProcessor.addStaticCollEntity(
LoadedColumnRow(currCol, currRow), this);
} else {
game.gameMap.collisionProcessor.addActiveCollEntity(this);
}
for (int i = 0; i < vertices.length; i++) {
if (vertices[i].x < _minCoords.x) {
_minCoords.x = vertices[i].x;
}
if (vertices[i].x > _maxCoords.x) {
_maxCoords.x = vertices[i].x;
}
if (vertices[i].y < _minCoords.y) {
_minCoords.y = vertices[i].y;
}
if (vertices[i].y > _maxCoords.y) {
_maxCoords.y = vertices[i].y;
}
}
width = _maxCoords.x - _minCoords.x;
height = _maxCoords.y - _minCoords.y;
_center = (_maxCoords + _minCoords) / 2;
transformPoint ??= _center;
}
Vector2 getMinVector()
{
if(isStatic){
return _minCoords;
}
var par = parent as PositionComponent;
if(par.isFlippedHorizontally){
return Vector2(_getPointFromRawPoint(_maxCoords).x, _getPointFromRawPoint(_minCoords).y);
}else{
return _getPointFromRawPoint(_minCoords);
}
}
Vector2 getMaxVector()
{
if(isStatic){
return _maxCoords;
}
var par = parent as PositionComponent;
if(par.isFlippedHorizontally){
return Vector2(_getPointFromRawPoint(_minCoords).x, _getPointFromRawPoint(_maxCoords).y);
}else{
return _getPointFromRawPoint(_maxCoords);
}
}
doDebug(Color? color) {
for (int i = 0; i < vertices.length; i++) {
if(parent == null){
return;
}
PointCust p = PointCust(
position: getPoint(i), color: color);
game.gameMap.add(p);
}
}
@override
void onRemove() {
if (!isStatic) {
game.gameMap.collisionProcessor.removeActiveCollEntity(this);
} else {
game.gameMap.collisionProcessor.removeStaticCollEntity(_myCoords);
}
}
bool onComponentTypeCheck(DCollisionEntity other);
void onCollisionStart(Set<Vector2> intersectionPoints,
DCollisionEntity other);
void onCollisionEnd(DCollisionEntity other);
void onCollision(Set<Vector2> intersectionPoints, DCollisionEntity other);
Vector2 _getPointFromRawPoint(Vector2 rawPoint)
{
if(isStatic){
return rawPoint;
}else {
var temp = parent as PositionComponent;
Vector2 posAnchor = temp.positionOfAnchor(temp.anchor);
Vector2 point = rawPoint - transformPoint!;
if(temp.isFlippedHorizontally){
point.x *= -1;
}
point.x *= scale.x;
point.y *= scale.y;
if(angle != 0){
point = _rotatePoint(point,temp.isFlippedHorizontally);
}
point += transformPoint!;
return point + posAnchor;
}
}
Vector2 getCenter() {
return _getPointFromRawPoint(_center);
}
int getVerticesCount() {
return vertices.length;
}
Vector2 getPoint(int index) {
return _getPointFromRawPoint(vertices[index]);
}
Vector2 _rotatePoint(Vector2 point, bool isHorizontalFlip) {
double radian = angle * pi / 180;
isHorizontalFlip ? radian *= -1 : radian;
point.x = point.x * cos(radian) - point.y * sin(radian);
point.y = point.x * sin(radian) + point.y * cos(radian);
return point;
}
@override
void update(double dt) {
// doDebug();
super.update(dt);
}
}
Полный прекомпил исходной карты *.tmx в картинки, анимации и любые какие угодно объекты
Future compileAll(LoadedColumnRow colRow) async
{
if (colRow.column != 0 && colRow.row != 0) {
return;
}
var fileName = 'top_left_bottom-slice.tmx'; //Имя вашей карты из Tiled
var tiled = await TiledComponent.load(fileName, Vector2.all(320));
var layersLists = tiled.tileMap.renderableLayers;
if (true) {
MySuperAnimCompiler compilerAnimationBack = MySuperAnimCompiler();
MySuperAnimCompiler compilerAnimation = MySuperAnimCompiler();
for (var a in layersLists) {
if (a.layer.type != LayerType.tileLayer) {
continue;
}
await processTileType(
clear: false,
renderMode: RenderCompileMode.Background,
tileMap: tiled.tileMap,
addTiles: (tile, position, size) async {
compilerAnimationBack.addTile(position, tile);
},
layersToLoad: [a.layer.name]);
compilerAnimationBack.addLayer();
await processTileType(
clear: false,
renderMode: RenderCompileMode.Foreground,
tileMap: tiled.tileMap,
addTiles: (tile, position, size) async {
compilerAnimation.addTile(position, tile);
},
layersToLoad: [a.layer.name]);
compilerAnimation.addLayer();
}
print('start compile!');
await compilerAnimation.compile('high');
await compilerAnimationBack.compile('down');
}
// tiled = await TiledComponent.load(fileName, Vector2.all(320));
Set<String> loadedFiles = {};
for(var layer in layersLists){
if(layer.layer.type == LayerType.objectGroup){
var objs = tiled.tileMap.getLayer<ObjectGroup>(layer.layer.name);
if (objs != null) {
for (int cols = 0; cols < GameConsts.maxColumn; cols++) {
for (int rows = 0; rows < GameConsts.maxRow; rows++) {
var positionCurs = Vector2(
cols * GameConsts.lengthOfTileSquare.x,
rows * GameConsts.lengthOfTileSquare.y);
String newObjs = '';
Rectangle rec = Rectangle.fromPoints(positionCurs, Vector2(
positionCurs.x + GameConsts.lengthOfTileSquare.x,
positionCurs.y + GameConsts.lengthOfTileSquare.y));
for (final obj in objs.objects) {
if (obj.name == '') {
continue;
}
Rectangle objRect = Rectangle.fromPoints(
Vector2(obj.x, obj.y),
Vector2(obj.x + obj.width, obj.y + obj.height));
if (isIntersect(rec, objRect)) {
newObjs +=
'<o nm="${obj.name}" cl="${obj.type}" x="${obj
.x}" y="${obj.y}" w="${obj.width}" h="${obj
.height}"';
for(final props in obj.properties){
newObjs += ' ${props.name}="${props.value}"';
}
newObjs += '/>';
newObjs += '\n';
}
}
if (newObjs != '') {
File file = File('assets/metaData/$cols-$rows.objXml');
if (!loadedFiles.contains('assets/metaData/$cols-$rows.objXml')) {
loadedFiles.add('assets/metaData/$cols-$rows.objXml');
file.writeAsStringSync('<p>\n', mode: FileMode.append);
}
file.writeAsStringSync(newObjs, mode: FileMode.append);
}
}
}
}
print('END OF OBJS COMPILE');
print('start grounds compile');
if (objs != null) {
Map<LoadedColumnRow, List<GroundSource>> objsMap = {};
for (final obj in objs.objects) {
if (obj.name != '') {
continue;
}
bool isLoop = false;
List<Vector2> points = [];
if (obj.isPolygon) {
isLoop = true;
for (final point in obj.polygon) {
points.add(Vector2(point.x + obj.x, point.y + obj.y));
}
}
if (obj.isPolyline) {
for (final point in obj.polyline) {
points.add(Vector2(point.x + obj.x, point.y + obj.y));
}
}
if (obj.isRectangle) {
isLoop = true;
points.add(Vector2(obj.x, obj.y));
points.add(Vector2(obj.x, obj.y + obj.height));
points.add(Vector2(obj.x + obj.width, obj.y + obj.height));
points.add(Vector2(obj.x + obj.width, obj.y));
}
int minCol = GameConsts.maxColumn;
int minRow = GameConsts.maxRow;
int maxCol = 0;
int maxRow = 0;
for (final point in points) {
minCol = min(minCol, point.x ~/ (GameConsts.lengthOfTileSquare.x));
minRow = min(minRow, point.y ~/ (GameConsts.lengthOfTileSquare.y));
maxCol = max(maxCol, point.x ~/ (GameConsts.lengthOfTileSquare.x));
maxRow = max(maxRow, point.y ~/ (GameConsts.lengthOfTileSquare.y));
}
bool isReallyLoop = minCol == maxCol && minRow == maxRow && isLoop;
for (int currColInCycle = minCol; currColInCycle <= maxCol; currColInCycle++) {
for (int currRowInCycle = minRow; currRowInCycle <= maxRow; currRowInCycle++) {
Vector2 topLeft = Vector2(currColInCycle * GameConsts.lengthOfTileSquare.x,
currRowInCycle * GameConsts.lengthOfTileSquare.y);
Vector2 topRight = Vector2(
(currColInCycle + 1) * GameConsts.lengthOfTileSquare.x,
currRowInCycle * GameConsts.lengthOfTileSquare.y);
Vector2 bottomLeft = Vector2(currColInCycle * GameConsts.lengthOfTileSquare.x,
(currRowInCycle + 1) * GameConsts.lengthOfTileSquare.y);
Vector2 bottomRight = Vector2(
(currColInCycle + 1) * GameConsts.lengthOfTileSquare.x,
(currRowInCycle + 1) * GameConsts.lengthOfTileSquare.y);
List<Vector2> coord = [];
for (int i = -1; i < points.length - 1; i++) {
if (!isLoop && i == -1) {
continue;
}
int tF, tS;
if (i == -1) {
tF = points.length - 1;
tS = 0;
} else {
tF = i;
tS = i + 1;
}
List<Vector2> tempCoord = [];
if (points[tF].x >= topLeft.x && points[tF].x <= topRight.x
&& points[tF].y >= topLeft.y && points[tF].y <= bottomLeft
.y) {
coord.add(points[tF]);
}
Vector2 answer = f_pointOfIntersect(
topLeft, topRight, points[tF], points[tS]);
if (answer != Vector2.zero()) {
tempCoord.add(answer);
}
answer = f_pointOfIntersect(
topRight, bottomRight, points[tF], points[tS]);
if (answer != Vector2.zero()) {
tempCoord.add(answer);
}
answer = f_pointOfIntersect(
bottomRight, bottomLeft, points[tF], points[tS]);
if (answer != Vector2.zero()) {
tempCoord.add(answer);
}
answer = f_pointOfIntersect(
bottomLeft, topLeft, points[tF], points[tS]);
if (answer != Vector2.zero()) {
tempCoord.add(answer);
}
if (tempCoord.length == 1) {
coord.add(tempCoord[0]);
if(coord.length > 1){
GroundSource newPoints = GroundSource();
newPoints.isLoop = false;
newPoints.points = List.unmodifiable(coord);
objsMap.putIfAbsent(LoadedColumnRow(currColInCycle, currRowInCycle), () => []);
objsMap[LoadedColumnRow(currColInCycle, currRowInCycle)]!.add(newPoints);
coord.clear();
}
} else {
if (tempCoord.length == 2) {
coord.clear();
if (points[tF].distanceTo(tempCoord[0]) >
points[tF].distanceTo(tempCoord[1])) {
coord.add(tempCoord[1]);
coord.add(tempCoord[0]);
} else {
coord.add(tempCoord[0]);
coord.add(tempCoord[1]);
}
GroundSource newPoints = GroundSource();
newPoints.isLoop = false;
newPoints.points = List.unmodifiable(coord);
objsMap.putIfAbsent(LoadedColumnRow(currColInCycle, currRowInCycle), () => []);
objsMap[LoadedColumnRow(currColInCycle, currRowInCycle)]!.add(newPoints);
coord.clear();
} else if(tempCoord.length > 2){
print('CRITICAL ERROR IN PRECOMPILE GROUND!!!');
}
}
}
if (points[points.length - 1].x >= topLeft.x &&
points[points.length - 1].x <= topRight.x
&& points[points.length - 1].y >= topLeft.y &&
points[points.length - 1].y <= bottomLeft.y && !isLoop) {
coord.add(points[points.length - 1]);
}
if(coord.isNotEmpty) {
GroundSource newPoints = GroundSource();
newPoints.isLoop = isReallyLoop;
newPoints.points = List.unmodifiable(coord);
objsMap.putIfAbsent(
LoadedColumnRow(currColInCycle, currRowInCycle), () =>
[
]);
objsMap[LoadedColumnRow(currColInCycle, currRowInCycle)]!.add(
newPoints);
}
}
}
}
for(final key in objsMap.keys){
File file = File('assets/metaData/${key.column}-${key.row}.objXml');
if(!loadedFiles.contains('assets/metaData/${key.column}-${key.row}.objXml')){
file.writeAsStringSync('<p>\n', mode: FileMode.append);
loadedFiles.add('assets/metaData/${key.column}-${key.row}.objXml');
}
for(int i=0;i<objsMap[key]!.length;i++){
if(objsMap[key]![i].points.isEmpty){
continue;
}
file.writeAsStringSync('\n<o lp="${objsMap[key]![i].isLoop ? '1' : '0'}" nm="" p="', mode: FileMode.append);
for(int j=0;j<objsMap[key]![i].points.length;j++){
if(j > 0){
file.writeAsStringSync(' ', mode: FileMode.append);
}
file.writeAsStringSync('${objsMap[key]![i].points[j].x},${objsMap[key]![i].points[j].y}', mode: FileMode.append);
}
file.writeAsStringSync('"/>', mode: FileMode.append);
}
}
}
}
}
for(final key in loadedFiles){
File file = File(key);
file.writeAsStringSync('\n</p>', mode: FileMode.append);
}
}
Ну и собственно чтение и создание всех файлов на лету на наш игровой экран при изменении позиции персонажа:
Динамическая загрузка объектов
//custMap - это менеджер всех файлов, которые должны отображаться на экране.
//Имено он должен сам следить и удалять те элементы, которые пропали из вида
//Двигающиеся объекты удаляют себя сами. Поэтому надо проверять - не был ли он уже
//загружен
Future<void> generateMap(LoadedColumnRow colRow) async
{
if (colRow.column >= GameConsts.maxColumn || colRow.row >= GameConsts.maxRow) {
return;
}
if (colRow.column < 0 || colRow.row < 0) {
return;
}
custMap.allEls.putIfAbsent(colRow, () => []);
if (KyrgyzGame.cachedMapPngs.contains('${colRow.column}-${colRow.row}_down.png')) {
Image _imageDown = await Flame.images.load(
'metaData/${colRow.column}-${colRow.row}_down.png'); //KyrgyzGame.cachedImgs['$column-${row}_down.png']!;
var spriteDown = SpriteComponent(
sprite: Sprite(_imageDown),
position: Vector2(colRow.column * GameConsts.lengthOfTileSquare.x,
colRow.row * GameConsts.lengthOfTileSquare.y),
size: GameConsts.lengthOfTileSquare + Vector2.all(1),
priority: 0,
);
custMap.allEls[colRow]!.add(spriteDown);
custMap.add(spriteDown);
}
if (KyrgyzGame.cachedAnims.containsKey('${colRow.column}-${colRow.row}_down.anim')) {
var objects = KyrgyzGame.cachedAnims['${colRow.column}-${colRow.row}_down.anim']!;
for (final obj in objects) {
Vector2 sourceSize = Vector2(double.parse(obj.getAttribute('w')!),double.parse(obj.getAttribute('h')!));
Image srcImage = KyrgyzGame.cachedImgs[obj.getAttribute('src')!]!;
final List<Sprite> spriteList = [];
final List<double> stepTimes = [];
for (final anim in obj.findAllElements('fr')) {
spriteList.add(Sprite(srcImage, srcSize: sourceSize,
srcPosition: Vector2(
double.parse(anim.getAttribute('cl')!) * sourceSize.x,
double.parse(anim.getAttribute('rw')!) * sourceSize.y)));
stepTimes.add(double.parse(anim.getAttribute('dr')!));
}
var sprAnim = SpriteAnimation.variableSpriteList(
spriteList, stepTimes: stepTimes);
for (final anim in obj.findAllElements('ps')) {
var ss = SpriteAnimationComponent(animation: sprAnim,
position: Vector2(double.parse(anim.getAttribute('x')!),
double.parse(anim.getAttribute('y')!)),
size: Vector2(sourceSize.x+1, sourceSize.y+1),
priority: GamePriority.ground + 1);
custMap.allEls[colRow]!.add(ss);
custMap.add(ss);
}
}
}
if (KyrgyzGame.cachedMapPngs.contains('${colRow.column}-${colRow.row}_high.png')) {
Image _imageHigh = await Flame.images.load(
'metaData/${colRow.column}-${colRow.row}_high.png'); //KyrgyzGame.cachedImgs['$column-${row}_high.png']!;
var spriteHigh = SpriteComponent(
sprite: Sprite(_imageHigh),
position: Vector2(colRow.column * GameConsts.lengthOfTileSquare.x,
colRow.row * GameConsts.lengthOfTileSquare.y),
priority: GamePriority.high - 1,
size: GameConsts.lengthOfTileSquare + Vector2.all(1),
);
custMap.allEls[colRow]!.add(spriteHigh);
custMap.add(spriteHigh);
}
if (KyrgyzGame.cachedAnims.containsKey('${colRow.column}-${colRow.row}_high.anim')) {
var objects = KyrgyzGame.cachedAnims['${colRow.column}-${colRow.row}_high.anim']!;
for (final obj in objects) {
Vector2 srcSize = Vector2(double.parse(obj.getAttribute('w')!),double.parse(obj.getAttribute('h')!));
Image srcImage = KyrgyzGame.cachedImgs[obj.getAttribute('src')!]!;
final List<Sprite> spriteList = [];
final List<double> stepTimes = [];
for (final anim in obj.findAllElements('fr')) {
spriteList.add(Sprite(srcImage, srcSize: srcSize,
srcPosition: Vector2(
double.parse(anim.getAttribute('cl')!) * srcSize.x,
double.parse(anim.getAttribute('rw')!) * srcSize.y)));
stepTimes.add(double.parse(anim.getAttribute('dr')!));
}
var sprAnim = SpriteAnimation.variableSpriteList(
spriteList, stepTimes: stepTimes);
for (final anim in obj.findAllElements('ps')) {
var ss = SpriteAnimationComponent(animation: sprAnim,
position: Vector2(double.parse(anim.getAttribute('x')!),
double.parse(anim.getAttribute('y')!)),
size: Vector2(srcSize.x+1, srcSize.y+1),
priority: GamePriority.high);
custMap.allEls[colRow]!.add(ss);
custMap.add(ss);
}
}
}
if (KyrgyzGame.cachedObjXmls.containsKey('${colRow.column}-${colRow.row}.objXml')) {
var objects = KyrgyzGame.cachedObjXmls['${colRow.column}-${colRow.row}.objXml']!;
for (final obj in objects) {
String? name = obj.getAttribute('nm');
switch (name) {
case '':
var points = obj.getAttribute('p')!;
var pointsList = points.split(' ');
List<Vector2> temp = [];
for(final sources in pointsList){
if(sources == ''){
continue;
}
temp.add(Vector2(double.parse(sources.split(',')[0]),double.parse(sources.split(',')[1])));
}
if(temp.isNotEmpty) {
var ground = Ground(temp, collisionType: DCollisionType.passive,
isSolid: false,
isStatic: true,
isLoop: obj.getAttribute('lp')! == '1',
game: myGame!,
column: colRow.column,
row: colRow.row);
custMap.allEls[colRow]!.add(ground);
custMap.add(ground);
}
break;
default:
_createLiveObj(obj, name, colRow);
break;
}
}
}
}
Future _createLiveObj(XmlElement obj, String? name, LoadedColumnRow colRow) async
{
Vector2 position = Vector2(
double.parse(obj.getAttribute('x')!),
double.parse(obj.getAttribute('y')!)
);
if (custMap.loadedLivesObjs.contains(position)) {
return;
}
switch (name) {
case 'enemy':
custMap.loadedLivesObjs.add(position);
custMap.add(GrassGolem(
position, GolemVariant.Grass, priority: GamePriority.player - 2));
break;
case 'wgolem':
custMap.loadedLivesObjs.add(position);
custMap.add(GrassGolem(
position, GolemVariant.Water, priority: GamePriority.player - 2));
break;
case 'gold':
var temp = LootOnMap(itemFromId(2), position: position);
custMap.allEls[colRow]!.add(temp);
custMap.add(temp);
break;
case 'strange_merchant':
var temp = StrangeMerchant(position,StrangeMerchantVariant.black, priority: GamePriority.player - 2);
custMap.allEls[colRow]!.add(temp);
custMap.add(temp);
break;
case 'chest':
var temp = Chest(1, myItems: [itemFromId(2)], position: position);
custMap.allEls[colRow]!.add(temp);
custMap.add(temp);
break;
case 'fObelisk':
var temp = FlyingHighObelisk(
position, colRow.column, colRow.row, priority: GamePriority.high - 1);
custMap.allEls[colRow]!.add(temp);
custMap.add(temp);
var temp2 = FlyingDownObelisk(
position, colRow.column, colRow.row, priority: GamePriority.player - 2);
custMap.allEls[colRow]!.add(temp2);
custMap.add(temp2);
break;
case 'sObelisk':
var temp = StandHighObelisk(position, priority: GamePriority.high - 1);
custMap.allEls[colRow]!.add(temp);
custMap.add(temp);
var temp2 = StandDownObelisk(
position, priority: GamePriority.player - 2);
custMap.allEls[colRow]!.add(temp2);
custMap.add(temp2);
break;
case 'telep':
var targetPos = obj.getAttribute('tar')!.split(',');
Vector2 target = Vector2(double.parse(targetPos[0]), double.parse(targetPos[1]));
Vector2 telSize = Vector2(double.parse(obj.getAttribute('w')!), double.parse(obj.getAttribute('h')!));
var temp = Teleport(size: telSize, position: position, targetPos: target);
custMap.allEls[colRow]!.add(temp);
custMap.add(temp);
}
}
Процесс нахождения коллизий
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/image_composition.dart';
import 'package:game_flame/abstracts/enemy.dart';
import 'package:game_flame/abstracts/hitboxes.dart';
import 'package:game_flame/abstracts/obstacle.dart';
import 'package:game_flame/abstracts/player.dart';
import 'package:game_flame/abstracts/utils.dart';
import 'package:game_flame/components/physic_vals.dart';
import 'package:game_flame/components/tile_map_component.dart';
class DCollisionProcessor
{
final List<DCollisionEntity> _activeCollEntity = [];
final Map<LoadedColumnRow,List<DCollisionEntity>> _staticCollEntity = {};
Map<LoadedColumnRow, List<DCollisionEntity>> _potentialActiveEntity = {};
Set<LoadedColumnRow> _contactNests = {};
void addActiveCollEntity(DCollisionEntity entity)
{
_activeCollEntity.add(entity);
}
void removeActiveCollEntity(DCollisionEntity entity)
{
_activeCollEntity.remove(entity);
}
void addStaticCollEntity(LoadedColumnRow colRow, DCollisionEntity entity)
{
_staticCollEntity.putIfAbsent(colRow, () => []);
_staticCollEntity[colRow]!.add(entity);
}
void removeStaticCollEntity(LoadedColumnRow? colRow)
{
_staticCollEntity.remove(colRow);
}
void clearActiveCollEntity()
{
_activeCollEntity.clear();
}
void clearStaticCollEntity()
{
_staticCollEntity.clear();
}
void updateCollisions()
{
_potentialActiveEntity.clear();
for(DCollisionEntity entity in _activeCollEntity){
entity.obstacleIntersects = {};
if(entity.collisionType == DCollisionType.inactive) {
continue;
}
_contactNests.clear();
int minCol = 0;
int maxCol = 0;
int minRow = 0;
int maxRow = 0;
for(int i=0;i<entity.getVerticesCount();i++){
if(i==0){
minCol = entity.getPoint(i).x ~/ GameConsts.lengthOfTileSquare.x;
maxCol = entity.getPoint(i).x ~/ GameConsts.lengthOfTileSquare.x;
minRow = entity.getPoint(i).y ~/ GameConsts.lengthOfTileSquare.y;
maxRow = entity.getPoint(i).y ~/ GameConsts.lengthOfTileSquare.y;
continue;
}
minCol = math.min(minCol, entity.getPoint(i).x ~/ GameConsts.lengthOfTileSquare.x);
maxCol = math.max(maxCol, entity.getPoint(i).x ~/ GameConsts.lengthOfTileSquare.x);
minRow = math.min(minRow, entity.getPoint(i).y ~/ GameConsts.lengthOfTileSquare.y);
maxRow = math.max(maxRow, entity.getPoint(i).y ~/ GameConsts.lengthOfTileSquare.y);
}
for(int col = minCol; col <= maxCol; col++){
for(int row = minRow; row <= maxRow; row++){
var lcr = LoadedColumnRow(col, row);
_potentialActiveEntity.putIfAbsent(lcr, () => []);
_potentialActiveEntity[lcr]!.add(entity);
_contactNests.add(lcr);
}
}
for(final lcr in _contactNests){
if(_staticCollEntity.containsKey(lcr)) {
for(final other in _staticCollEntity[lcr]!){
if(other.collisionType == DCollisionType.inactive){
continue;
}
if(entity.collisionType == DCollisionType.passive && other.collisionType == DCollisionType.passive){
continue;
}
if(entity.parent != null && entity.parent !is MainPlayer && other.onlyForPlayer){
continue;
}
if(!entity.onComponentTypeCheck(other) && !other.onComponentTypeCheck(entity)) {
continue;
}
_calcTwoEntities(entity, other, other is MapObstacle);
}
}
}
}
final listOfList = List.unmodifiable(_potentialActiveEntity.values.toList());
for(int key = 0; key < listOfList.length; key++){
Set<int> removeList = {};
for(int i = 0; i < listOfList[key].length; i++){
for(int j = 0; j < listOfList[key].length; j++){
if(i == j || removeList.contains(j)){
continue;
}
if(listOfList[key][i].collisionType == DCollisionType.passive && listOfList[key][j].collisionType == DCollisionType.passive){
continue;
}
if(!listOfList[key][i].onComponentTypeCheck(listOfList[key][j]) && !listOfList[key][j].onComponentTypeCheck(listOfList[key][i])){
continue;
}
if(listOfList[key][j] is MapObstacle){
if(listOfList[key][i].parent != null && listOfList[key][i].parent is KyrgyzEnemy && listOfList[key][j].onlyForPlayer){
continue;
}
_calcTwoEntities(listOfList[key][i], listOfList[key][j],true);
continue;
}
if(listOfList[key][i] is MapObstacle){
if(listOfList[key][j].parent != null && listOfList[key][j].parent is KyrgyzEnemy && listOfList[key][i].onlyForPlayer){
continue;
}
_calcTwoEntities(listOfList[key][j], listOfList[key][i],true);
continue;
}
_calcTwoEntities(listOfList[key][j], listOfList[key][i],false);
}
removeList.add(i);
}
}
for(DCollisionEntity entity in _activeCollEntity){
if(entity.obstacleIntersects.isNotEmpty){
entity.onCollisionStart(entity.obstacleIntersects, entity); ///obstacleIntersects есть только у ГроундХитбоксов, поэтому можно во втором аргументе передать лажу
entity.obstacleIntersects.clear();
}
}
}
}
//Активные entity ВСЕГДА ПРЯМОУГОЛЬНИКИ залупленные
void _calcTwoEntities(DCollisionEntity entity, DCollisionEntity other, bool isMapObstacle)
{
Set<int> insidePoints = {};
if(isMapObstacle) { //Если у вас все обекты столкновения с препятсвием с землёй квадратные, иначе делать что ближе от центра твоего тела
for (int i = 0; i < other.getVerticesCount(); i++) {
Vector2 otherFirst = other.getPoint(i);
if(otherFirst.x <= entity.getMaxVector().x
&& otherFirst.x >= entity.getMinVector().x
&& otherFirst.y <= entity.getPoint(1).y
&& otherFirst.y >= entity.getPoint(0).y){
insidePoints.add(i);
}
}
}
_finalInterCalc(entity, other,insidePoints,isMapObstacle);
}
void _finalInterCalc(DCollisionEntity entity, DCollisionEntity other,Set<int> insidePoints, bool isMapObstacle)
{
for (int i = -1; i < other.getVerticesCount() - 1; i++) {
if (!other.isLoop && i == -1) {
continue;
}
int tFirst, tSecond;
if (i == -1) {
tFirst = other.getVerticesCount() - 1;
} else {
tFirst = i;
}
tSecond = i + 1;
Vector2 otherFirst = other.getPoint(tFirst);
Vector2 otherSecond = other.getPoint(tSecond);
if (isMapObstacle) {
if(insidePoints.contains(tFirst) && insidePoints.contains(tSecond)) {
entity.obstacleIntersects.add((otherFirst + otherSecond) / 2);
continue;
}
List<Vector2> tempBorderLines = [];
for(int i= - 1; i<entity.getVerticesCount() - 1; i++){
if (!entity.isLoop && i == -1) {
continue;
}
int tF, tS;
if (i == -1) {
tF = entity.getVerticesCount() - 1;
} else {
tF = i;
}
tS = i + 1;
Vector2 point = f_pointOfIntersect(entity.getPoint(tF), entity.getPoint(tS)
, otherFirst, otherSecond);
if (point != Vector2.zero()) {
tempBorderLines.add(point);
}
}
if (tempBorderLines.length == 2) {
entity.obstacleIntersects.add((tempBorderLines[0] + tempBorderLines[1]) / 2);
}else if(tempBorderLines.length == 1){
Vector2 absVec;
if(insidePoints.contains(tFirst)){
absVec = tempBorderLines[0] + otherFirst;
}else{
absVec = tempBorderLines[0] + otherSecond;
}
absVec /= 2;
entity.obstacleIntersects.add(absVec);
}
} else {
for(int i=-1; i<entity.getVerticesCount() - 1; i++){
if (!entity.isLoop && i == -1) {
continue;
}
int tF, tS;
if (i == -1) {
tF = entity.getVerticesCount() - 1;
tS = i + 1;
}else{
tF = i;
tS = i + 1;
}
Vector2 tempPos = f_pointOfIntersect(entity.getPoint(tF), entity.getPoint(tS), otherFirst, otherSecond);
if(tempPos != Vector2.zero()){
if (entity.onComponentTypeCheck(other)) {
entity.onCollisionStart({otherFirst}, other);
}
if (other.onComponentTypeCheck(entity)) {
other.onCollisionStart({otherFirst}, entity);
}
return;
}
}
}
}
}
//return point of intersects of two lines from 4 points
При моих оптимизациях при 100 движущихся и ещё 20 статичных объектах FPS на телефоне — 30. Это, я думаю, предел оптимизаций, однако такое количество врагов вряд ли вообще нужно в игре:
![](https://habrastorage.org/getpro/habr/upload_files/120/9c0/425/1209c0425bd1c2363f569214e84030ee.jpg)
![](https://habrastorage.org/getpro/habr/upload_files/c5d/2af/fbc/c5d2affbcc6557cf0d9508d16edee66d.jpg)
Если кому-то поможет моя статья — буду рад.