avant-propos
Component
Après la mise en œuvre de la détection de collision et l'ajout de l'effet de rétroaction de la collision, l'ensemble de l'effet est temporairement fermé. Cet article se concentrera sur 敌机Component
le travail de refactoring . Profitez-en pour ajouter le reste des genres敌机Component
.
L'auteur a inclus cette série d'articles dans les colonnes suivantes, et les étudiants intéressés sont invités à lire :
Notes de développement pour Aircraft Wars basées sur Flutter & Flame
abstrait
La classe précédente Enemy1
est un type 敌机Component
, qui a les fonctions de base de l'avion ennemi dans la guerre aérienne :
- En cas d'absence de collision , déplacez-vous du haut de l'écran vers le bas de l'écran à vitesse constante , et enfin retirez-vous de celui-ci.
Component树
- Il a la capacité de détecter les collisions , et
战机Component/子弹Component
lorsqu'une collision se produit, il y aura une réduction de la santé . - Lorsque la valeur de santé est 0 , l' effet de destruction/destruction est généré.
En fait, il n'y a pas non plus d' 战机Component/子弹Component
effet de réduction de la santé en cas de collision. En combinant les points ci-dessus, nous devons ajouter une variété 敌机Component
à l'écran. Par conséquent 敌机Component
, les caractéristiques doivent être davantage résumées .
Combiné avec les caractéristiques ci-dessus, défini 抽象类Enemy
.
SpriteAnimationComponent
Parlons d'abord du schéma de lecture des images d'animation dans différents états. SpriteAnimationComponent
La capacité de lecture que nous avons utilisée précédemment 敌机Component
a été définie lorsqu'elle a été détruite playing = true
. Mais à ce moment il y a au 敌机Component
moins 3 états, qui sont normaux, attaqués et détruits ... Une énumération peut être définie.
enum EnemyState {
idle,
hit,
down,
}
复制代码
Ici peut être utilisé à la SpriteAnimationGroupComponent
place SpriteAnimationComponent
, en définissant des paramètres current
pour changer l'état actuel , commutant ainsi l'effet d'animation correspondant. Peut-être jeter un œil au code source
// sprite_animation_group_component.dart
class SpriteAnimationGroupComponent<T> extends PositionComponent
with HasPaint
implements SizeProvider {
/// Key with the current playing animation
T? current;
/// Map with the mapping each state to the flag removeOnFinish
final Map<T, bool> removeOnFinish;
/// Map with the available states for this animation group
Map<T, SpriteAnimation>? animations;
复制代码
Ici 范型T
peut être défini comme défini ci-dessus EnemyState
. animations
correspondant à différents étatsSpriteAnimation
. Il y en a un autre removeOnFinish
, on peut comprendre qu'une fois la lecture de quel état terminée, le composant est automatiquement supprimé .
// sprite_animation_group_component.dart
SpriteAnimation? get animation => animations?[current];
@mustCallSuper
@override
void render(Canvas canvas) {
animation?.getSprite().render(
canvas,
size: size,
overridePaint: paint,
);
}
@mustCallSuper
@override
void update(double dt) {
animation?.update(dt);
if ((removeOnFinish[current] ?? false) && (animation?.done() ?? false)) {
removeFromParent();
}
}
复制代码
render
La méthode obtiendra l'état correspondant SpriteAnimation
à render . update
Il détectera si l'animation est terminée et si l'état doit être automatiquement supprimé. ps : render
La méthode est un rappel pour dessiner chaque image.
Demande de statut
Dans la méthode de construction, l'état initial est idle
, l' down
état de réglage est automatiquement supprimé une fois la lecture terminée .
// class Enemy
Enemy(
{required Vector2 initPosition,
required Vector2 size,
required this.life,
required this.speed})
: super(
position: initPosition,
size: size,
current: EnemyState.idle,
removeOnFinish: {EnemyState.down: true}) {
animations = <EnemyState, SpriteAnimation>{};
}
复制代码
Définissez trois méthodes abstraites pour charger différents états . SpriteAnimation
Étant donné hit
que tous les états 敌机Component
n'en ont pas, ils sont définis comme nullables ici.
// class Enemy
Future<SpriteAnimation> idleSpriteAnimation();
Future<SpriteAnimation?> hitSpriteAnimation();
Future<SpriteAnimation> downSpriteAnimation();
复制代码
Charger en onLoad
. Notez qu'une hit
fois la lecture de cet état terminée, l'état doit être réinitialisé à l' idle
état .
// abstract class Enemy
@override
Future<void> onLoad() async {
animations?[EnemyState.idle] = await idleSpriteAnimation();
final hit = await hitSpriteAnimation();
hit?.onComplete = () {
_enemyState = EnemyState.idle;
};
if (hit != null) animations?[EnemyState.hit] = hit;
animations?[EnemyState.down] = await downSpriteAnimation();
。。。
复制代码
en détection de collision
- 如果已经是
down
状态了,就无需触发等待动画播放完自动移除。 - 如果碰撞目标为
Player/Bullect1
,则需要处理,生命值未到达0前状态更改为hit
,否则为down
。hit
播放完需要变更回idle
,与上述逻辑对应上。
// class Enemy
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (current == EnemyState.down) return;
if (other is Player || other is Bullet1) {
if (current == EnemyState.idle) {
if (life > 1) {
_enemyState = EnemyState.hit;
life--;
} else {
_enemyState = EnemyState.down;
life = 0;
}
。。。
复制代码
状态变成前,需要重置将要变更状态的SpriteAnimation
。这是为了保证每次变更都是从第一帧开始,不会造成画面异常。
// class Enemy
set _enemyState(EnemyState state) {
if (state == EnemyState.hit) {
animations?[state]?.reset();
}
current = state;
}
复制代码
Component的移动
之前是通过s = v * t
,在update
方法回调中更新position
的方式实现移动的。这里改成使用MoveEffect
实现。
// class Enemy
add(MoveEffect.to(
Vector2(position.x, gameRef.size.y), EffectController(speed: speed),
onComplete: () {
removeFromParent();
}));
复制代码
传入speed
,会使用SpeedEffectController
,默认是线性移动的。
// effect_contorller.dart
final isLinear = curve == Curves.linear;
if (isLinear) {
items.add(
duration != null
? LinearEffectController(duration)
: SpeedEffectController(LinearEffectController(0), speed: speed!),
);
}
复制代码
这部分可参考官方示例: flame/aseprite_example.dart at main · flame-engine/flame (github.com)
新一代敌机Component
简单说一下重构后的敌机Component
,这里以第二个类型类Enemy2
为例,因为它的生命值高可以触发hit
状态。
// class Enemy2
@override
Future<SpriteAnimation?> hitSpriteAnimation() async {
List<Sprite> sprites = [];
sprites.add(await Sprite.load('enemy/enemy2_hit.png'));
sprites.add(await Sprite.load('enemy/enemy2.png'));
final spriteAnimation =
SpriteAnimation.spriteList(sprites, stepTime: 0.15, loop: false);
return spriteAnimation;
}
@override
RectangleHitbox rectangleHitbox() {
return RectangleHitbox(
size: Vector2(size.x, size.y * 0.9), position: Vector2(0, 0));
}
复制代码
- 以重写
hit
状态的SpriteAnimation
加载为例,这里有一帧的被击中效果。 - 还需要输出一个
RectangleHitbox
,由于不同素材的尺寸有误差,所以这里单独作碰撞箱的修正。
大部分逻辑都在父类Enemy
实现,这里基本只需要实现抽象方法即可。
敌机生成器适配
Vous vous souvenez qu'il y en avait un avant 敌机生成器EnemyCreator
, pour la création de timing 敌机Component
? En raison de l'ajout de différents types d'avions ennemis, sa méthode de déclenchement de synchronisation _createEnemy
doit être modifiée en conséquence. Avant cela, nous devons définir les attributs de chaque avion ennemi . 1, 2 et 3 représentent les classes Enemy1、Enemy2、Enemy3
, à savoir les types petit, moyen et grand. Voir le texte pour la valeur de l'attribut.
// class EnemyCreator
final enemyAttrMapping = {
1: EnemyAttr(size: Vector2(45, 45), life: 1, speed: 50.0),
2: EnemyAttr(size: Vector2(50, 60), life: 2, speed: 30.0),
3: EnemyAttr(size: Vector2(100, 150), life: 4, speed: 20.0)
};
复制代码
_createEnemy
, nous contrôlons la probabilité de génération de chaque type par intervalle .
void _createEnemy() {
final width = gameRef.size.x;
double x = _random.nextDouble() * width;
final double random = _random.nextDouble();
final EnemyAttr attr;
final Enemy enemy;
if (random < 0.5) {
// load Enemy1
} else if (random >= 0.5 && random < 0.8) {
// load Enemy2
} else {
// load Enemy3
}
add(enemy);
}
复制代码
À ce stade, 敌机Component
la refactorisation est terminée et il y aura quelques petits changements dans le suivi. Jetons un coup d'œil à l'effet actuel
Résumer
敌机Component
La refactorisation est terminée, et les règles de génération de synchronisation peuvent être un peu approximatives, et une optimisation peut être envisagée dans ce suivi. Concernant les attributs de l'avion ennemi, il est actuellement écrit à mort, et il peut être considéré comme une configuration locale à l'avenir.