prefacio
Component
Después de implementar la detección de colisión y agregar el efecto de retroalimentación de la colisión, todo el efecto se cierra temporalmente. Este artículo se centrará en 敌机Component
el trabajo de refactorización . Aprovecha esta oportunidad para agregar el resto de los géneros敌机Component
.
El autor ha incluido esta serie de artículos en las siguientes columnas, y los estudiantes interesados pueden leer:
Notas de desarrollo para Aircraft Wars basadas en Flutter&Flame
resumen
La clase anterior Enemy1
es un tipo 敌机Component
, que tiene las funciones básicas de los aviones enemigos en la guerra de aviones:
- En el caso de que no haya colisión , muévase desde la parte superior de la pantalla hasta la parte inferior de la pantalla a una velocidad constante y, finalmente, retírela.
Component树
- Tiene la capacidad de detectar colisiones , y
战机Component/子弹Component
cuando ocurre una colisión, habrá una reducción en la salud . - Cuando el valor de salud es 0 , se genera el efecto de destrucción/destrucción .
De hecho, también falta un 战机Component/子弹Component
efecto de reducción de la salud al chocar con él. Combinando los puntos anteriores, necesitamos agregar una variedad 敌机Component
a la pantalla. Por lo tanto 敌机Component
, las características deben resumirse aún más .
Combinado con las características anteriores, definido 抽象类Enemy
.
SpriteAnimationComponent
Primero hablemos sobre el esquema de reproducción de cuadros de animación en diferentes estados. SpriteAnimationComponent
La capacidad de reproducción que usamos anteriormente 敌机Component
se estableció cuando se destruyó playing = true
. Pero en este momento hay al 敌机Component
menos 3 estados, que son normal, atacado y destruido, se puede definir una enumeración.
enum EnemyState {
idle,
hit,
down,
}
复制代码
Aquí se puede usar SpriteAnimationGroupComponent
en su lugar SpriteAnimationComponent
, configurando parámetros current
para cambiar el estado actual , cambiando así el efecto de animación correspondiente. Tal vez eche un vistazo al código fuente.
// 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;
复制代码
Aquí 范型T
se puede configurar como se definió anteriormente EnemyState
. animations
correspondientes a diferentes estadosSpriteAnimation
. Hay otro removeOnFinish
, se puede entender que después de que se completa la reproducción de qué estado, el Componente se elimina automáticamente .
// 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
El método obtendrá el estado correspondiente SpriteAnimation
para renderizar . Detectará si la animación está completaupdate
y si el estado debe eliminarse automáticamente. pd: el método es una devolución de llamada para dibujar cada cuadro.render
Solicitud de estado
En el método de construcción, el estado inicial es idle
, el down
estado de configuración se elimina automáticamente una vez que se completa la reproducción .
// 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>{};
}
复制代码
Defina tres métodos abstractos para cargar diferentes estados . SpriteAnimation
Dado que hit
no todos los estados los 敌机Component
tienen, aquí se definen como anulables.
// class Enemy
Future<SpriteAnimation> idleSpriteAnimation();
Future<SpriteAnimation?> hitSpriteAnimation();
Future<SpriteAnimation> downSpriteAnimation();
复制代码
Cargar en onLoad
. Tenga en cuenta que hit
una vez completada la reproducción de este estado, el estado debe restablecerse al idle
estado .
// 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 detección de colisión
- 如果已经是
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
实现,这里基本只需要实现抽象方法即可。
敌机生成器适配
¿Recuerdas que hubo uno antes 敌机生成器EnemyCreator
, para cronometrar la creación 敌机Component
? Debido a la adición de diferentes tipos de aeronaves enemigas, su método de activación de tiempo _createEnemy
debe modificarse en consecuencia. Antes de eso, necesitamos definir los atributos de cada avión enemigo . 1, 2 y 3 representan clases Enemy1、Enemy2、Enemy3
, es decir, tipos pequeño, mediano y grande. Consulte el texto para el valor del atributo.
// 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
, controlamos la probabilidad de generación de cada tipo por intervalo .
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);
}
复制代码
En este punto, 敌机Component
la refactorización ha llegado a su fin y habrá algunos pequeños cambios en el seguimiento. Echemos un vistazo al efecto actual.
Resumir
敌机Component
La refactorización está completa, y las reglas para la generación de tiempo pueden ser un poco toscas, y se puede considerar la optimización en este seguimiento. En cuanto a los atributos del avión enemigo, actualmente está escrito a muerte, y puede considerarse como una configuración local en el futuro.