TankCombat系列文章
如果你还不了解Flame可以看这里:
Flutter&Flame——TankCombat游戏开发(一)
Flutter&Flame——TankCombat游戏开发(二)
Flutter&Flame——TankCombat游戏开发(三)
Flutter&Flame——TankCombat游戏开发(四)
效果图
蛮好看的,我再加一下,让大家整体有个印象自己在做什么 :)
image开工
本章节,我们开始制作发射炮弹和敌方坦克的设计
开火
还记得这段代码吗?
//发射按钮
Row(
children: [
SizedBox(width: 48),
FireButton(
onTap: tankGame.onFireButtonTap,
),
Spacer(),
FireButton(
onTap: tankGame.onFireButtonTap,
),
SizedBox(width: 48),
],
),
在main函数中的runApp方法中,这两个是我们的开火按钮,可以看到,点击事件触发了game中的onFireButtonTap方法,我们来看看具体实现:
void onFireButtonTap(){
if(blueBulletNum < 20){
bullets.add(Bullet(this,BulletColor.BLUE,tank.tankId
,position: tank.getBulletOffset(),angle: tank.getBulletAngle()));
}
}
为了避免子弹过多,导致的卡顿,这里加了个玩家子弹上限,下面就是往bullets(list)加了一颗子弹,同时传给了这颗子弹坦克的位置和game对象,另外两个参数先不用管。我们先看看bullet这个类
Bullet
首先我们还是让bullet集成baseComponent,并创建一些变量,你可以将子弹抽象成坦克,这样看他们本质就没啥区别了,甚至更简单一些
如下:
class Bullet extends BaseComponent{
final TankGame game;
final double speed;//子弹速度
Offset position;//子弹位置
double angle = 0;//子弹角度
bool isOffScreen = false;//是否飞出屏幕
//玩家坦克的子弹图片
final Sprite blueSprite = Sprite('tank/bullet_blue.webp'),
//是否击中
bool isHit = false;
}
这样一些子弹的基础属性就声明完成了,接下来我们在render方法和update方法中操纵子弹即可。
先看update:
@override
void update(double t) {
//我们首先判断是否已经飞出屏幕/击中敌人,这样我们就没必要操作它了,
if(isHit) return;
if(isOffScreen)return;
//之后我们按照既定角度和速度来更新子单位置以达到飞行的效果
//子弹角度是由坦克炮塔角度决定的
position = position + Offset.fromDirection(angle,speed * t);
//下面的方法就比较容易理解了,判断是不是飞出了屏幕,并更新isOffScreen
if (position.dx < -50) {
isOffScreen = true;
}
if (position.dx > game.screenSize.width + 50) {
isOffScreen = true;
}
if (position.dy < -50) {
isOffScreen = true;
}
if (position.dy > game.screenSize.height + 50) {
isOffScreen = true;
}
}
再看render:
@override
void render(Canvas canvas) {
//理论上讲这里不写也没事,我个人倾向不写的,大家可以看一下flame的流程图就明白了
if(isHit) return;
if(isOffScreen)return;
canvas.save();
//方法很简单,将画布移动到子单位制和旋转对应角度
canvas.translate(position.dx, position.dy);
canvas.rotate(angle);
//然后绘制子弹即可
blueSprite.renderRect(canvas, Rect.fromLTWH(-4, -2, 8, 4));
canvas.restore();
}
ok,这样子弹就处理完了,现在我们回到game中
TankGame
所有的component都需要与game联系起来,不然是没法进行更新和渲染上屏的(仅指游戏)。
因为我们肯定不止一发子弹,所以我们创建一个list
List<Bullet> bullets; //炮弹
接着在resize中实例化它
@override
void resize(Size size) {
screenSize = size;
//initEnemyTank();
if(bg == null){
bg = BattleBackground(this);
}
if(tank == null){
tank = Tank(
this,position: Offset(screenSize.width/2,screenSize.height/2),
);
}
if(bullets == null){
bullets = List();
}
}
然后在update中我们将关键参数 t 传给它,并调用子弹的update方法
@override
void update(double t) {
bullets.forEach((element) {
//子弹
element.update(t);
}
//移除飞出屏幕的
bullets.removeWhere((element) => element.isHit || element.isOffScreen);
}
我们在render方法中调用子弹的render方法,并将canvas传给它
@override
void render(Canvas canvas) {
bg.render(canvas);
//tank
tank.render(canvas);
//bullet
bullets.forEach((element) {
element.render(canvas);
});
}
这样我们就完成了坦克发射炮弹的功能,我们来梳理一下大致流程:
image以上图片也可以帮助你理解component/sprite 在game中的工作流程。
现在我们虽然可以发射炮弹,但是没法打到人,换言之,我们需要先添加一些敌人。
敌军坦克TankModel
敌军坦克和玩家坦克有很多功能可以共用,我们先给敌军坦克抽象出来一个模型 TankModel :
abstract class TankModel{
final int id;
final TankGame game;
Sprite bodySprite,turretSprite;
//出生位置
Offset position;
TankModel(this.game,this.bodySprite,this.turretSprite,this.position):
id = DateTime.now().millisecondsSinceEpoch+Random().nextInt(100);
///随机生成路线用到
final int seedNum = 50;
final int seedRatio = 2;
//移动的路线
double movedDis = 0;
//直线速度
final double speed = 80;
//转弯速度
final double turnSpeed = 40;
//车体角度
double bodyAngle = 0;
//炮塔角度
double turretAngle = 0;
//车体目标角度
double targetBodyAngle;
//炮塔目标角度
double targetTurretAngle;
//tank是否存活
bool isDead = false;
//移动到目标位置
Offset targetOffset;
final double ration = 0.7;
///获取炮弹发射位置
Offset getBulletOffset() ;
///炮弹角度
double getBulletAngle();
}
都是一堆属性,没啥好说的。现在我们根据模型开始造坦克了,这里我们就造一个绿色的敌方坦克吧
GreenTank
首先我们继承tankModel,然后混入baseComponent,如下:
class GreenTank extends TankModel with BaseComponent{
//坦克身体
Rect bodyRect ;
//坦克炮管
Rect turretRect;
GreenTank(TankGame game, Sprite bodySprite, Sprite turretSprite,Offset position)
: super(game, bodySprite, turretSprite,position){
bodyRect = Rect.fromLTWH(-20*ration, -15*ration, 38*ration, 32*ration);
turretRect = Rect.fromLTWH(-1, -2*ration, 22*ration, 6*ration);
generateTargetOffset();
}
void generateTargetOffset(){
double x = Random().nextDouble() * (game.screenSize.width - (seedNum * seedRatio));
double y = Random().nextDouble() * (game.screenSize.height - (seedNum * seedRatio));
targetOffset = Offset(x,y);
Offset temp = targetOffset - position;
targetBodyAngle = temp.direction;
targetTurretAngle = temp.direction;
}
@override
void render(Canvas canvas) {
if(isDead) return;
drawBody(canvas);
}
@override
void update(double t) {
rotateBody(t);
rotateTurret(t);
moveTank(t);
}
}
构造函数我们初始化了一些基本属性,这个在上文已经介绍过,不再赘述。我们看多出的这个方法
void generateTargetOffset(){
double x = Random().nextDouble() * (game.screenSize.width - (seedNum * seedRatio));
double y = Random().nextDouble() * (game.screenSize.height - (seedNum * seedRatio));
targetOffset = Offset(x,y);
Offset temp = targetOffset - position;
targetBodyAngle = temp.direction;
targetTurretAngle = temp.direction;
}
这个方法用于生成一个随机的目标点,然后让坦克开过去,根据目标点,我们把目标角度(炮塔和车身)保存下来。
render和update方法中的函数跟之前基本一样,唯一区别在moveTank(t)这个方法,代码如下:
void moveTank(double t) {
if(targetBodyAngle != null){
if(targetOffset != null){
//可以看到这里多了一个 movedDis, 用来存储走了多少距离
movedDis += speed * t;
if(movedDis < 100){
if(bodyAngle == targetBodyAngle){
//tank 直线时 移动速度快
position = position + Offset.fromDirection(bodyAngle,speed*t);//100 是像素
}else{
//tank旋转时 移动速度要慢
position = position + Offset.fromDirection(bodyAngle,turnSpeed*t);
}
}else{
//当行驶距离超出100时我们重新计算新的目标点
movedDis = 0;
generateTargetOffset();
}
}
}
}
经过了上面的开动,我们的敌军坦克就不再是‘头铁直奔南墙’了,而是走一段距离就会自己转弯,更为灵活生动了。
ok,敌军坦克完成了,我们开始将他们和game组合
组合启动
TankGame
我们在game中增加两个list分别管理两种颜色的敌军坦克
List<GreenTank> gTanks = [];
List<SandTank> sTanks = [];
之后我们在tankGame构造函数初始化中初始化4个敌军坦克
TankGame(){
observer = GameObserver(this);
initEnemyTank();
}
///初始化敌军
void initEnemyTank() {
var turretSprite = Sprite('tank/t_turret_green.webp');
var bodySprite= Sprite('tank/t_body_green.webp');
gTanks.add(GreenTank(this,bodySprite,turretSprite, Offset(100,100)));
gTanks.add(GreenTank(this,bodySprite,turretSprite, Offset(100,screenSize.height*0.8)));
///sand
var turretSpriteS = Sprite('tank/t_turret_sand.webp');
var bodySpriteS = Sprite('tank/t_body_sand.webp');
sTanks.add( SandTank(this,bodySpriteS,turretSpriteS,
Offset(screenSize.width-100,100)));
sTanks.add( SandTank(this,bodySpriteS,turretSpriteS,
Offset(screenSize.width-100,screenSize.height*0.8)));
}
经过上面的操作我们的仓库gTanks和sTanks里面就各有两台整装待发的坦克了,现在开动它们!
在update和render方法中我们增加下面的代码:
update
gTanks.forEach((element) {
element.update(t);
});
sTanks.forEach((element) {
element.update(t);
});
//移除死亡tank
gTanks.removeWhere((element) => element.isDead);
sTanks.removeWhere((element) => element.isDead);
render
gTanks.forEach((element) {
element.render(canvas);
});
sTanks.forEach((element) {
element.render(canvas);
});
功能上文已经说过了。
现在运行一下就可以看到4个敌军小坦克满地图跑了,不过还不会开炮,我们来增加一下这个功能。
电脑开炮功能
首先我们考虑蓝、绿、黄三个坦克炮弹不同,且后期可能加别的功能,我们为了区分,先给bullet类文件增加一个枚举:
enum BulletColor{
BLUE,GREEN,SAND
}
之后我们在game中增加一个敌军坦克开火的方法:
void enemyTankFire<T extends TankModel>(BulletColor color,T tankModel){
bullets.add(Bullet(this,color,tankModel.id
,position: tankModel.getBulletOffset(),angle: tankModel.getBulletAngle()));
}
原理和玩家坦克开火一样,为了避免炮弹过多造成卡顿(打不过电脑),我们给敌军增加一下子弹上限
game中增加两个变量
//黄色炮弹数量
int sandBulletNum = 0;
//蓝色炮弹数量
int blueBulletNum = 0;
game的update方法中统计一下在屏的炮弹数量
blueBulletNum = 0;
greenBulletNum = 0;
sandBulletNum = 0;
bullets.forEach((element) {
switch(element.bulletColor){
case BulletColor.BLUE:
blueBulletNum ++;
break;
case BulletColor.GREEN:
greenBulletNum ++;
break;
case BulletColor.SAND:
sandBulletNum ++;
break;
}
element.update(t);
});
之后我们在敌军坦克 greenTank的update方法中增加两行代码:
@override
void update(double t) {
rotateBody(t);
rotateTurret(t);
moveTank(t);
//当没达到上限时,我们就发射一枚炮弹
if(game.greenBulletNum < 10){
game.enemyTankFire(BulletColor.GREEN, this);
}
}
现在我们运行一下,就会看到满地飞奔,四处转动炮塔开火的敌军小坦克了!
ok,大功过半,马上告成,在下一章我们将增加炮弹击毁坦克的功能和爆炸效果以及GameObserver的设计。
多谢阅读,喜欢的点个赞吧 :)
网友评论