TankCombat系列文章
如果你还不了解Flame可以看这里:
Flutter&Flame——TankCombat游戏开发(一)
Flutter&Flame——TankCombat游戏开发(二)
Flutter&Flame——TankCombat游戏开发(三)
Flutter&Flame——TankCombat游戏开发(四)
效果图
蛮好看的,我再加一下,让大家整体有个印象自己在做什么 :)
开工
闲话你bo要讲——接上回,我们这次开始完成控制坦克的功能
角度pi的正负示意图
X轴从右到左,为从正向负
y轴从上到下,为从正向负
pi的范围是 (-pi,pi)
Tank代码结构
class Tank extends BaseComponent{
final int tankId = 666;
final TankGame game;
Sprite bodySprite,turretSprite;
Tank(this.game,{this.position}){
turretSprite = Sprite('tank/t_turret_blue.webp');
bodySprite= Sprite('tank/t_body_blue.webp');
}
//出生位置
Offset position;
//车体角度
double bodyAngle = 0;
//炮塔角度
double turretAngle = 0;
//车体目标角度
double targetBodyAngle;
//炮塔目标角度
double targetTurretAngle;
//tank是否存活
bool isDead = false;
final double ration = 0.7;
@override
void render(Canvas canvas) {
if(isDead) return;
drawBody(canvas);
}
@override
void update(double t) {
//时间增量t 旋转速率
rotateBody(t);
rotateTurret(t);
moveTank(t);
}
}
控制坦克
我们在Tank里面增加几个变量用于控制和计算坦克及炮塔的角度
//车体角度
double bodyAngle = 0;
//炮塔角度
double turretAngle = 0;
//车体目标角度
double targetBodyAngle;
//炮塔目标角度
double targetTurretAngle;
//tank是否存活
bool isDead = false;
然后我们更新一下drawBody方法,增加坦克身体和炮塔的旋转绘制,
void drawBody(Canvas canvas) {
//操作前保存一下状态
canvas.save();
canvas.translate(position.dx, position.dy);
//车身旋转的角度
canvas.rotate(bodyAngle);
//绘制tank身体
bodySprite.renderRect(canvas,Rect.fromLTWH(-20*ration, -15*ration, 38*ration, 32*ration));
//旋转炮台
canvas.rotate(turretAngle);
// 绘制炮塔
turretSprite.renderRect(canvas, Rect.fromLTWH(-1, -2*ration, 22*ration, 6*ration));
canvas.restore();
}
同时,之前空出的update方法,此时就派上用场了,我们在其中增加三个方法:
rotateBody(t);//旋转身体
rotateTurret(t);//旋转炮塔
moveTank(t);//移动坦克
注意这个t是double类型,表示的是时间增量,举个栗子,如果是60fps,那么这个t理论上=0.01666
源码如下:
@override
void update(double t) {
//时间增量t
rotateBody(t);
rotateTurret(t);
moveTank(t);
}
这三个方法比较长,且繁琐,我将说明加在注释里面以便于联系阅读
旋转车身
void rotateBody(double t) {
//旋转车身是不能 'chua'一下就转过去的,得有一个平滑的过程
//因此这里根据pi * t 算出一个比较平均的旋转速率rotationRate
final double rotationRate = pi * t;
//在做旋转之前,我们要确定 目标角度是不能为空的
if (targetBodyAngle != null) {
//车身角度 小于 目标角度
if (bodyAngle < targetBodyAngle) {
//车体角度和目标角度差额,判断是否大于/小于极值(-pi,pi)
//正向或者逆向旋转
if ((targetBodyAngle - bodyAngle).abs() > pi) {
//大于,以rotationRate来减去车身角度
bodyAngle = bodyAngle - rotationRate;
//如果车身角度小于 负极值,那么给他额外+加一圈,
//这样保证了值在集合内,同时坐标方位角不变
//如果这里不好理解,可以看一下上面的示意图
if (bodyAngle < -pi) {
bodyAngle += pi * 2;
}
} else {
//小于 (与上同理)
bodyAngle = bodyAngle + rotationRate;
if (bodyAngle > targetBodyAngle) {
bodyAngle = targetBodyAngle;
}
}
}
//车身角度 大于 目标角度
//同理,只是操作相反
if (bodyAngle > targetBodyAngle) {
if ((targetBodyAngle - bodyAngle).abs() > pi) {
bodyAngle = bodyAngle + rotationRate;
if (bodyAngle > pi) {
bodyAngle -= pi * 2;
}
} else {
bodyAngle = bodyAngle - rotationRate;
if (bodyAngle < targetBodyAngle) {
bodyAngle = targetBodyAngle;
}
}
}
}
}
旋转炮塔
void rotateTurret(double t) {
final double rotationRate = pi * t;
if(targetTurretAngle != null){
//这里的原理和旋转车身是一样的,唯一的区别就是我们的炮塔目标角度,并不是直接使用 变量 targetTurretAngle
//而是通过targetTurretAngle - bodyAngle 来算出真正的目标角度
//这是因为,在drawBody()方法中,我们先对 车身进行了旋转,所以这里要减去
double localTargetTurretAngle = targetTurretAngle - bodyAngle;
if(turretAngle < localTargetTurretAngle){
if((localTargetTurretAngle -turretAngle).abs() > pi){
turretAngle = turretAngle - rotationRate;
//超出临界值,进行转换 即:小于-pi时,转换成(pi-差值)之后继续累加,具体参考 笛卡尔坐标,范围是(-pi,pi)
if(turretAngle < -pi){
turretAngle += pi*2;
}
}else{
turretAngle = turretAngle + rotationRate;
if(turretAngle > localTargetTurretAngle){
turretAngle = localTargetTurretAngle;
}
}
}
if(turretAngle > localTargetTurretAngle){
if((localTargetTurretAngle - turretAngle).abs() > pi){
turretAngle = turretAngle + rotationRate;
if(turretAngle > pi){
turretAngle -= pi*2;
}
}else{
turretAngle = turretAngle - rotationRate;
if(turretAngle < localTargetTurretAngle){
turretAngle = localTargetTurretAngle;
}
}
}
}
}
移动坦克
//这里方法比较简单,
//100和50 分别是坦克的速度,因为转弯的时候慢,所以转弯速度为50,直行为100
void moveTank(double t) {
if(targetBodyAngle != null){
if(bodyAngle == targetBodyAngle){
//tank 直线时 移动速度快
position = position + Offset.fromDirection(bodyAngle,100*t);//100 是像素
}else{
//tank旋转时 移动速度要慢
position = position + Offset.fromDirection(bodyAngle,50*t);
}
}
}
至此坦克控制系统就写完了,接下来就是连接摇杆和坦克了。
组合启动
还记得mian函数中的代码吗?
main()
final TankGame tankGame = TankGame();
runApp(Directionality(textDirection: TextDirection.ltr,
child: Stack(
children: [
tankGame.widget,
Column(
children: [
Spacer(),
//发射按钮
Row(
children: [
SizedBox(width: 48),
FireButton(
onTap: tankGame.onFireButtonTap,
),
Spacer(),
FireButton(
onTap: tankGame.onFireButtonTap,
),
SizedBox(width: 48),
],
),
SizedBox(height: 20),
//摇杆
Row(
children: [
SizedBox(width: 48),
JoyStick(
onChange: (Offset delta)=>tankGame.onLeftJoypadChange(delta),
),
Spacer(),
JoyStick(
onChange: (Offset delta)=>tankGame.onRightJoypadChange(delta),
),
SizedBox(width: 48)
],
),
SizedBox(height: 24)
],
),
],
)));
可以看到我们将摇杆的回调中传出来的偏移量delta,通过game的两个方法
onLeftJoypadChange(delta)//车身
onRightJoypadChange(delta)//炮塔
传给了game,用来更新游戏component
TankGame
我们将tankgame的代码进行一下更新,首先增加一个玩家坦克
Tank tank;
在增加两个方法接收摇杆的delta,我们将delta的方向传给tank
void onLeftJoypadChange(Offset offset){
if(offset == Offset.zero){
tank.targetBodyAngle = null;
}else{
//得到车身的目标角度
tank.targetBodyAngle = offset.direction;//范围(pi,-pi)
}
}
void onRightJoypadChange(Offset offset) {
if (offset == Offset.zero) {
tank.targetTurretAngle = null;
} else {
//得到炮塔目标角度
tank.targetTurretAngle = offset.direction;
}
}
下面,我们在resize()方法中实例化一下tank,一般情况下这个方法只会在屏幕尺寸变化时才会调用
if(tank == null){
tank = Tank(
//出生点在屏幕中央
this,position: Offset(screenSize.width/2,screenSize.height/2),
);
}
好的,目前摇杆的控制已经与坦克连接到一起了,但是控制效果我们是看不到的,因为没有根据屏幕的刷新进行渲染和更新。
我们只需要在game的render和update调用tank对应的方法即可。
@override
void render(Canvas canvas) {
if(screenSize == null)return;
//绘制草坪
bg.render(canvas);
//tank
tank.render(canvas);
}
@override
void update(double t) {
if(screenSize == null)return;
tank.update(t);
}
至此整个坦克控制系统就完成了,你可以运行一下,活动活动坦克了,下一章我们将进行炮弹和敌方坦克的设计,
谢谢你的阅读。 :)