Last active
October 13, 2020 13:16
-
-
Save GitLeandroHub/b4621519d2f6e6c929c61a47031f8735 to your computer and use it in GitHub Desktop.
Simple Space-shooter game in Flutter using *Flame* game engine from Scratch. (All code in main.dart, no good habits and modularization)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flame/game.dart'; | |
import 'package:flame/time.dart'; | |
//util é async, entao alterar o main para async | |
import 'package:flame/flame.dart'; | |
//utilizar FlameAnimation como Alias para não dar conflito com animation do Dart | |
import 'package:flame/animation.dart' as FlameAnimation; | |
import 'dart:math'; | |
import 'package:flame/spritesheet.dart'; | |
//scrcpy pra fazer o display do aparelho android caso não tenha emulador virtual https://github.com/Genymobile/scrcpy | |
//Todo jodo do flame "vira" um widget do flutter | |
void main() async { | |
//await Flame.util.setPortrait(); | |
//await Flame.util.fullScreen(); | |
Size size = await Flame.util.initialDimensions(); | |
//passar size para widget para repassar para o game | |
runApp(GameWidget(size)); | |
} | |
class GameWidget extends StatelessWidget { | |
final Size size; | |
GameWidget(this.size); | |
@override | |
Widget build(BuildContext context) { | |
//inicializar o game numa variável | |
final game = SpaceShooterGame(size); | |
return GestureDetector( | |
onPanStart: (_) { | |
game.beginFire(); | |
}, | |
onPanEnd: (_) { | |
game.stopFire(); | |
}, | |
onPanCancel: () { | |
game.stopFire(); | |
}, | |
//registrar eventos | |
onPanUpdate: (DragUpdateDetails details) { | |
//delta vai retornar um offset de quanto mudou nessa tragetória do update dele | |
game.onPlayerMove(details.delta); | |
}, | |
child: Container(color: Color(0xFFFFFFFF), child: game.widget), | |
); | |
} | |
} | |
//para adicionar uma lista chamada "collidingObjects" de objetos que estão colidindo | |
class AnimationCollidableGameObject extends AnimationGameObject { | |
List<AnimationCollidableGameObject> collidingObjects = []; | |
} | |
class AnimationGameObject { | |
Rect position; | |
FlameAnimation.Animation animation; | |
void render(Canvas canvas) { | |
//verificar se o arquivo está carreagado em background | |
if (animation.loaded()) { | |
//verificado, esse método retorna o sprite/frame atual e chama um renderRect e passa canvas e position | |
animation.getSprite().renderRect(canvas, position); | |
} | |
} | |
//animação muda de frame baseado no tempo, entao adicionar o método update | |
void update(double deltaTime) { | |
animation.update(deltaTime); | |
} | |
} | |
//Game é a classe mais basica do flame, ela dá o game loop | |
class SpaceShooterGame extends Game { | |
final Size screenSize; | |
Random random = Random(); | |
//criar uma constante que delimita quanto o NPC se move por segundo | |
static const enemy_speed = 150; | |
static const shoot_speed = -500; | |
static const star_speed = 10; | |
AnimationGameObject player; | |
//classe importada utilitaria do Flame para criar algo que vai executar periodicamente | |
//instanciar timer: | |
Timer enemyCreator; | |
Timer shootCreator; | |
Timer starCreator; | |
//Criar os NPCs e instanciar | |
//atualizar os objetos para serem do tipo "Collidable" | |
List<AnimationCollidableGameObject> enemies = []; | |
List<AnimationCollidableGameObject> shoots = []; | |
List<AnimationCollidableGameObject> explosions = []; | |
List<AnimationCollidableGameObject> stars = []; | |
SpriteSheet starsSpritesheet; | |
//construtor | |
SpaceShooterGame(this.screenSize) { | |
player = AnimationGameObject() | |
..position = Rect.fromLTWH(175, 600, 72, 60) | |
..animation = FlameAnimation.Animation.sequenced("player.png", 4, | |
textureWidth: 24, textureHeight: 20); | |
//sequence é um dos construtores da animação do Flame que cria uma animação em função dos frames | |
//instanciar o enemy creator | |
//Timer recebe obrigatoriamente o tempo e depois adicionar booleano falando se ele irá se repetir (true, pois será periódico) e uma funcao callback para cada vez que houver um intervalo ele executa | |
enemyCreator = Timer(1.0, repeat: true, callback: () { | |
//nextDouble torna o número de zero a um | |
enemies.add(AnimationCollidableGameObject() | |
..animation = FlameAnimation.Animation.sequenced("enemy.png", 1, | |
textureWidth: 128, textureHeight: 128) | |
..position = Rect.fromLTWH( | |
(screenSize.width - 50) * random.nextDouble(), 0, 50, 50)); | |
}); | |
//pois irá executar desde o começo do jogo | |
enemyCreator.start(); | |
//instanciar/inicializar o shoot tambem | |
shootCreator = Timer(0.5, repeat: true, callback: () { | |
shoots.add(AnimationCollidableGameObject() | |
..animation = FlameAnimation.Animation.sequenced("bullet.png", 1, | |
textureWidth: 24, textureHeight: 20) | |
..position = Rect.fromLTWH( | |
player.position.left + 20, player.position.top - 20, 20, 20)); | |
}); | |
final starGapTime = (screenSize.height / 10) / star_speed; | |
starCreator = Timer(starGapTime, repeat: true, callback: () { | |
createRowOfStars(0); | |
}); | |
starCreator.start(); | |
//inicializar Spritesheet no construtor | |
starsSpritesheet = SpriteSheet( | |
imageName: "stars.png", | |
textureWidth: 38, | |
textureHeight: 38, | |
columns: 4, | |
rows: 4); | |
createInitialStars(); | |
} | |
//criar um "mestre" no game que recebe o quanto ele mudou | |
//alterar a posição do player de acordo com o offset | |
void onPlayerMove(Offset delta) { | |
player.position = player.position.translate(delta.dx, delta.dy); | |
} | |
//criar dois metodos: beginFire e stopFire | |
void beginFire() { | |
shootCreator.start(); | |
} | |
void stopFire() { | |
shootCreator.stop(); | |
} | |
//criar os métodos para as explosões | |
void createExplosionAt(double x, double y) { | |
final animation = FlameAnimation.Animation.sequenced("explosion.png", 6, | |
textureWidth: 32, textureHeight: 32, stepTime: 0.05) | |
..loop = false; | |
explosions.add(AnimationCollidableGameObject() | |
..animation = animation | |
..position = Rect.fromLTWH(x - 25, y - 25, 20, 20)); | |
} | |
void createStarAt(double x, double y) { | |
final animation = starsSpritesheet.createAnimation(random.nextInt(3), to: 4) | |
..variableStepTimes = [max(20, 100 * random.nextDouble()), 0.1, 0.1, 0.1]; | |
stars.add(AnimationCollidableGameObject() | |
..position = Rect.fromLTWH(x, y, 10, 10) | |
..animation = animation); | |
} | |
void createRowOfStars(double y) { | |
final gapSize = 6; | |
double starGap = screenSize.height / gapSize; | |
for (var i = 0; i < gapSize; i++) { | |
createStarAt(starGap * i + (random.nextDouble() * starGap), | |
y + (random.nextDouble() * 20)); | |
} | |
} | |
void createInitialStars() { | |
final gapSize = 10; | |
double rows = screenSize.height / gapSize; | |
for (var i = 0; i < gapSize; i++) { | |
createRowOfStars(i * rows); | |
} | |
} | |
@override | |
//game loop, processa a logica do jogo: movimento, colisão... | |
//delta time (em segundos) = quanto tempo se passou desde a ultima atualização | |
//flutter almeja 60 frames por segundo. Portanto esse update será chamado 60 vezes por segundo com delta time representando muito proximo desse | |
void update(double deltaTime) { | |
// e como o Timer é vinculado ao tempo do jogo precisa chamar o enemyCreator.update passando o deltatime para ficar vinculado ao fps do jogo | |
enemyCreator.update(deltaTime); | |
shootCreator.update(deltaTime); | |
starCreator.update(deltaTime); | |
player.update(deltaTime); | |
enemies.forEach((enemy) { | |
//atualizar o método update, pois agora o enemy também tem animação | |
enemy.update(deltaTime); | |
//alterar a posição [translate (x, y)] do NPC de acordo com a Velocidade e multiplicar pelo delta para que ele tenha a velocidade relativa para esse frame | |
enemy.position = enemy.position.translate(0, enemy_speed * deltaTime); | |
}); | |
shoots.forEach((shoot) { | |
shoot.update(deltaTime); | |
shoot.position = shoot.position.translate(0, shoot_speed * deltaTime); | |
}); | |
explosions.forEach((explosion) { | |
explosion.update(deltaTime); | |
}); | |
stars.forEach((star) { | |
star.update(deltaTime); | |
star.position = star.position.translate(0, star_speed * deltaTime); | |
}); | |
//para todos os tiros /passar para todos os enimigos e verificar se houve colisao | |
shoots.forEach((shoot) { | |
enemies.forEach((enemy) { | |
//pelo proprio método do Rect que recebe um proprio rect e retorna um booleano para identificar se houve colisão ou não | |
if (shoot.position.overlaps(enemy.position)) { | |
//se houve colisão | |
createExplosionAt(shoot.position.left, shoot.position.top); | |
shoot.collidingObjects.add(enemy); | |
enemy.collidingObjects.add(shoot); | |
} | |
}); | |
}); | |
//limpar o estado para não manter tanto objeto na memória e não crashar o jogo | |
//remover quando sair da tela ou || quando colidir com alguma coisa | |
enemies.removeWhere((enemy) { | |
return enemy.position.top >= screenSize.height || | |
enemy.collidingObjects.isNotEmpty; | |
}); | |
shoots.removeWhere((shoot) { | |
return shoot.position.bottom <= 0 || shoot.collidingObjects.isNotEmpty; | |
}); | |
explosions.removeWhere((explosion) => explosion.animation.isLastFrame); | |
stars.removeWhere((star) => star.position.top >= screenSize.height); | |
} | |
@override | |
//renderiza o estado atual do frame. Recebe o Canvas provido pela propria API do flutter | |
//chama o player.render para ser o canvas | |
void render(Canvas canvas) { | |
player.render(canvas); | |
//renderizar os NPCs | |
enemies.forEach((enemy) { | |
enemy.render(canvas); | |
}); | |
shoots.forEach((shoot) { | |
shoot.render(canvas); | |
}); | |
explosions.forEach((explosion) { | |
explosion.render(canvas); | |
}); | |
stars.forEach((star) { | |
star.render(canvas); | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: flame_primeiro | |
description: A new Flame Flutter application. | |
# The following line prevents the package from being accidentally published to | |
# pub.dev using `pub publish`. This is preferred for private packages. | |
publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |
# The following defines the version and build number for your application. | |
# A version number is three numbers separated by dots, like 1.2.43 | |
# followed by an optional build number separated by a +. | |
# Both the version and the builder number may be overridden in flutter | |
# build by specifying --build-name and --build-number, respectively. | |
# In Android, build-name is used as versionName while build-number used as versionCode. | |
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning | |
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. | |
# Read more about iOS versioning at | |
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | |
version: 1.0.0+1 | |
environment: | |
sdk: ">=2.7.0 <3.0.0" | |
dependencies: | |
flame: ^0.26.0 | |
flutter: | |
sdk: flutter | |
# The following adds the Cupertino Icons font to your application. | |
# Use with the CupertinoIcons class for iOS style icons. | |
cupertino_icons: ^1.0.0 | |
dev_dependencies: | |
flutter_test: | |
sdk: flutter | |
# For information on the generic Dart part of this file, see the | |
# following page: https://dart.dev/tools/pub/pubspec | |
//Necessário para o Flutter encontrar as imagens. O Flame por padrão busca as imagens em /assets | |
# The following section is specific to Flutter. | |
flutter: | |
assets: | |
- assets/images/player.png | |
- assets/images/enemy.png | |
- assets/images/explosion.png | |
- assets/images/bullet.png | |
- assets/images/stars.png |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment