使用Flutter和SpriteWidget开发第一个跨平台Android/IOS手机游戏 – Beginner Tutorial

基于Flutter和Flame游戏开发引擎学习资料,Create a Mobile Game with Flutter and Flame – Beginner Tutorial,对于更加适用于复杂一点手机游戏的Flutter 开发的2D游戏引擎SpriteWidget也制作了boxGame游戏例子,希望通过起始能够对于SpriteWidget有比较好的了解和掌握,如果能够帮助到你,别忘了给我点赞哦!

如果你有什么问题,可以发邮件给我,或者在Github上留言,方便的话,我会尽力帮助你。

你在了解的过程,也可以参考下面的文章:

1. Create a Mobile Game with Flutter and Flame – Beginner Tutorial

2.飞行射击游戏spritewidget/spaceblast

3.Flutter高性能复杂游戏2D开发游戏引擎spritewidget

前提条件:

1. Android Studio - Flutter开发工具,当然你也可以使用其他的,但本例使用AS开发。

2.Flutter SDK/Framework - AS开发插件,你需要具备开发Flutter的AS开发环境,如果你还没有掌握Flutter的开发基础,请先尝试Flutter开发学习。

你可以在Github找到这个练习的完整代码。

开始撸代码吧:

Step 1: 创建一个Flutter Application(略,希望你是了解Flutter的)

Step 2: 添加spritewidget插件以及清理Application

打开./pubspec.yamland,增加下面的内容在thecupertino_icons行下面并且在derdependencies分类之下(注意缩进).

spritewidget:

然后记得执行flutter packages get,或者点击AS界面上的Packages Get来添加插件。

下一步是清理代码,Flutter Project新创建的是一个example,打开./lib/main.dart,清空代码只保留void main() {},并且确保使用material library来运行runApp() .

2.1 空程序

然后删除./test目录,不然会出现错误,这里我们用不上test方法。

Step 3:  添加主程序

打开./lib/main.dart,添加一下代码,和Flutter创建一个新的界面主程序一样:

import 'package:flutter/material.dart';

import 'package:spritewidget/spritewidget.dart';

main () async {

  runApp(MyApp());

}

class MyAppextends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      title:"spriteWidget Game Eniger",

      theme: ThemeData(

      primarySwatch: Colors.blue,

      ),

      home: BoxGameScene(),

    );

  }

}

class BoxGameSceneextends StatefulWidget {

  @override

  State createState() => BoxGameSceneState();

}

class BoxGameSceneStateextends State {

  void initState(){

    super.initState();

  }

  @override

  Widget build(BuildContext context) {

    return null;

  }

}

这样就创建了一个Flutter的主程序。

Step 4: 添加游戏根节点RootNode:

新建一个dart文件命名为box_game.dart,创建class继承NodeWithSize,import游戏包:

import 'package:spritewidget/spritewidget.dart';

class BoxGame extends NodeWithSize {

  BoxGame() :super(new Size(320.0,320.0)){

  }

}

说明:创建一个BoxGame集成NodeWithSize作为RootNode,这样就可以实现spritewidget游戏引擎的游戏loop。一个基本的游戏loop在NodeWithSize以及继承的Node中可以通过update()和paint()方法来实现。

update()方法实现游戏node的移动或者更新(比如timer)。

paint()方法实现游戏node的绘制。

这里有一个非常重要的概念,spritewidget产生了自己的cooridinate system,使用Node自身坐标系进行对象位置的处理,因此:

BoxGame() :super(new Size(320.0,320.0)){}

就是给BoxGame这个Node初始化一个320*320尺寸的作为Node的自身坐标系,这样就可以按照这个坐标系进行新的子Node(child)的添加,每个Node,无论parent/child,都可以有自己自身的坐标系,以及起始位置相对于parent的坐标位置。

这个会在文后详细的把自己研究的坐标系相关信息进行描述。

Step 4: 添加RootNode到主程序

在main.dart中引入box_game.dart,然后在主程序app的state中初始化一个NodeWithSize,然后返回spritewidget():

import 'package:boxgamespritewidget/box_game.dart';

NodeWithSize _game;

_game =new BoxGame();

return SpriteWidget(_game);

这是main.dart就像下面的代码:

import 'package:flutter/material.dart';

import 'package:spritewidget/spritewidget.dart';

import 'package:boxgamespritewidget/box_game.dart';

main () async {

  runApp(MyApp());

}

class MyAppextends StatelessWidget {

  @override

  Widget build(BuildContext context) {

    return MaterialApp(

      title:"spriteWidget Game Eniger",

      theme: ThemeData(

      primarySwatch: Colors.blue,

      ),

      home: BoxGameScene(),

    );

  }

}

class BoxGameSceneextends StatefulWidget {

  @override

  State createState() => BoxGameSceneState();

}

class BoxGameSceneStateextends State {

  NodeWithSize _game;

  void initState(){

    super.initState();

    _game =new BoxGame();

  }

  @override

  Widget build(BuildContext context) {

    return SpriteWidget(_game);

  }

}

这时候你的app已经是个游戏app了。你可以运行一下看看。

Step 5: 绘制游戏主界面

添加游戏主节点

spritewidget添加对象非常简单,只需要在parentNode中addChild(childNode)就可以了,当然,需要你首先定义childNode。

我们在RootNode中添加一个游戏主界面_gameScreen,打开box_game.dart, 在BoxGame初始化时添加childNode。

Node_gameScreen;

BoxGame() :super(new Size(320.0,320.0)){

  _gameScreen =new Node();

  addChild(_gameScreen);

}

在rootNode中添加一个_gameScreen,是为了更好的在各Node进行交互时能够更有层次,方便不同parentNode和childNode的不同更新和绘制。

添加虚拟游戏操纵杆

spritewidget有一个炫酷的Node称之为VirtualJoystick,可以通过这个组件来控制游戏对象的输入,比如个方向在按住屏幕是的移动。

打开box_game.dart,相同于添加_gameScreen的方法,但是把_gameScreen作为父节点,将VirtualJoystick添加到_gameScreen中,

VirtualJoystick_joystick;

BoxGame() :super(new Size(320.0,320.0)){

  _gameScreen =new Node();

  addChild(_gameScreen);

  _joystick =new VirtualJoystick();

  _gameScreen.addChild(_joystick);

}

这时候你可以运行程序看看,你会发现VirtualJoystick并不存在,怎么回事?

原来,我们定义的RootNode使用了一个size(320, 320)的区域作为自己的坐标系统,那么对于不同尺寸的手机屏幕,并不是320x320的,这样就会根据rootNode的定义区域将屏幕进行适配,这时候VirtualJoystick缺省会被添加到屏幕的最下方,因此,我们需要初始化320x320

上的_gameScreen,设置它为最下端的屏幕Node,需要重新设置_gameScreen.position,

打开box_game.dart,在class BoxGame 中override方法spriteBoxPerformedLayout(),

@override

void spriteBoxPerformedLayout() {

  _gameScreen.position =new Offset(0.0,spriteBox.visibleArea.height);

}

让_gameScreen.position设置为基于ParentNode的高度方向坐标为y设置为spriteBox.visibleArea.height, 也就是320,向上平移320的高度。spriteBox.visibleArea之的是屏幕可见部分在320x320父节点中显示的部分,具体值可以参考后续说明。

这时再运行程序,一个很不错的游戏操纵杆在界面上显示了。

绘制并添加一个Box

新建一个BoxNode类集成Node,然后在绘制一个正方形,打开box_gam.dart,在BoxGame类下面创建一个BoxNode类,也可以新生成一个dart文件来创建BoxNode,然后在box_gam.dart中引用。

class BoxNode extends Node {

  BoxNode() {
    position = new Offset(0, 0);
  }

  @override
  void paint(Canvas canvas) {
       
  }
}

和BoxGame一样,BoxNode除了继承Node类以外,同样可以初始化相对于parentNode的position = new Offset(0,0); 通过paint()和update()进行绘制和更新。

说明:NodeWithSize实际上继承Node,但是增加了size和pivot点,可以通过更好的尺寸和支点来对Node进行更新和绘制。适合作为ParentNode或者RootNode,各个游戏对象可以使用Node创建。

在BoxNode中使用paint()进行绘制一个正方形,

@override
void paint(Canvas canvas) {

  boxRect = Rect.fromLTWH(
      spriteBox.visibleArea.height / 2 - 15,
      - spriteBox.visibleArea.height / 2 - 75,
      30,
      30,
  );

  Paint boxPaint = Paint();
  boxPaint.color = Color(0xff00ff00);

  canvas.drawRect(boxRect, boxPaint);
}

说明:基本上来讲,如果child的中点就是parent的size的中点减去偏移(前提是child的position初始化为new Offset(0, 0)。绘制对象使用canvas进行,对象和Paint()就可以实时绘制对象。

然后将BoxNode实例化,并添加到BoxGame的_gameScreen游戏主节点中,在BoxGame类的BoxGame() :super(new Size(320.0,320.0)){}初始化的_gameScreen下面添加,

BoxNode_box;

BoxGame() : super(new Size(320.0, 320.0)){

  _gameScreen = new Node();

  addChild(_gameScreen);

  _joystick = new VirtualJoystick();

  _gameScreen.addChild(_joystick);

  _box = new BoxNode();

  _gameScreen.addChild(_box);

}

OK,这时候运行程序,一个绿色的box以及一个虚拟游戏操纵杆就会出现在游戏主界面。

Step 6: 处理虚拟游戏操作

我们要通过虚拟游戏操纵杆来控制box的移动,需要在主界面update()中来通过VirtualJoystick来根据VirtualJoystick.value来改变box.position.

给BoxNode添加根据VirtualJoystick.value来更新自身position的方法,在class BoxNode中添加,

void applyThrust(Offset joystickValue) {

  Offset oldPos = position;

  Offset target = new Offset(joystickValue.dx * 160.0, joystickValue.dy * 220.0);

  double filterFactor = 0.2;

  position = new Offset(

      GameMath.filter(oldPos.dx, target.dx, filterFactor),

      GameMath.filter(oldPos.dy, target.dy, filterFactor));

}

说明:可以看到首先获取BoxNode的当前position作为旧的oldPos, 然后根据VirtualJoystick.value计算VirtualJoystick滑动屏幕的移动亮并根据屏幕尺寸进行放大,这里使用x放大160,y放大220,基本上是根据测试结果,会让VirtualJoystick操作box时看着比较流畅。

GameMath.filter方法可以在oldPos和target之间按照一个0-1之间的filterFactor插入多个移动位置,而不是直接将Box对象从oldPos移动到target,这样在更新的时候就会产生Box连续移动的效果,移动效果会比较流畅。

然后在BoxGame中,override主游戏RootNode的update()方法,将box的position的改变进行更新,这样就可以是的Box在屏幕上根据VirtualJoystick的操作进行移动,

void update(double dt) {

  _box.applyThrust(_joystick.value);

}

这样整个box_game.dart就像下面的代码,

import 'dart:ui';

import 'package:flutter/gestures.dart';

import 'package:spritewidget/spritewidget.dart';

class BoxGame extends NodeWithSize {

  // Game screen nodes

  Node _gameScreen;

  VirtualJoystick _joystick;

  BoxNode _box;

  double _scroll = 0.0;

  BoxGame() : super(new Size(320.0, 320.0)){

    _gameScreen = new Node();

    addChild(_gameScreen);

    _joystick = new VirtualJoystick();

    _gameScreen.addChild(_joystick);

    _box = new BoxNode();

    _gameScreen.addChild(_box);

  }

  @override

  void spriteBoxPerformedLayout() {

    _gameScreen.position = new Offset(0.0, spriteBox.visibleArea.height);

  }

  void update(double dt) {

    _box.applyThrust(_joystick.value);

  }

}

class BoxNode extends Node {

  bool hasWon = false;

  Rect boxRect;

  BoxNode() {

    position = new Offset(0, 0);

  }

  @override

  void paint(Canvas canvas) {

    boxRect = Rect.fromLTWH(

        spriteBox.visibleArea.height / 2 - 15,

        - spriteBox.visibleArea.height / 2 - 75,

        30,

        30,

    );

    Paint boxPaint = Paint();

    boxPaint.color = Color(0xff00ff00);

    canvas.drawRect(boxRect, boxPaint);

  }

  void applyThrust(Offset joystickValue) {

    Offset oldPos = position;

    Offset target = new Offset(joystickValue.dx * 160.0, joystickValue.dy * 220.0);

    double filterFactor = 0.2;

    position = new Offset(

        GameMath.filter(oldPos.dx, target.dx, filterFactor),

        GameMath.filter(oldPos.dy, target.dy, filterFactor));

  }

}

好了,运行一下程序,你可以通过VirtualJoystick来控制Box的移动了,是不是很酷?

Step 7: 添加点击事件控制box变色展示win状态

我们希望游戏对象可以被点击操作,spritewidget使用handleEvent(SpriteBoxEvent event){}来处理输入交互,SpriteBoxEvent包含多种点击屏幕处理事件,我们这里需要使用PointerDownEvent事件,当Box被点中时进行变色。

如果需要实现点击事件,需要对于点击检测Node进行设置,允许点击检测对象可以进行交互,但是不允许多点碰触,我们希望通过对BoxGame作为RootNode进行对象检测,如果发现在RootNode中点击到的区域是box的区域,则表明box被点中了,然后处理box被点击方法,首先在BoxGame初始化的时候BoxGame() :super(new Size(320.0,320.0)){}设置BoxGame定义为,

userInteractionEnabled =true;

handleMultiplePointers =false;

然后在BoxGame类中添加override方法,

@override

bool handleEvent(SpriteBoxEvent event) {

  if (event.type == PointerDownEvent) {

    Offset newPoint = convertPointToNodeSpace(event.boxPosition);

    if(newPoint.dx > _box.boxRect.left &&

        newPoint.dx < _box.boxRect.left + 30 &&

        newPoint.dy > _box.boxRect.top + spriteBox.visibleArea.height &&

        newPoint.dy < _box.boxRect.top + spriteBox.visibleArea.height + 30){

        //do box actions.

    }else{

      return false;

    }

  }

  return true;

}

说明:当点击事件发生时,判断是否为屏幕被点中,这是由于点中的位置为屏幕缺省坐标点,如果需要和RootNode的childNode的范围进行比较,需要首先使用convertPointToNodeSpace()方法将缺省坐标点转换为RootNode坐标系统,然后再和box的范围进行比较,由于_gameScreen作为主Node,初始化为new Offset(0.0, spriteBox.visibleArea.height),所以在对点击的位置判断是,需要把_box的位置也添加相同的偏移进行对比。

添加点击位置判断是box范围时,处理box的方法,在BoxNode类中定义,

bool hasWon =false;

定义一个布尔参数来分辨点击每次的状态,然后在paint()函数中,更改

boxPaint.color = Color(0xff00ff00);

布尔参数判读来使用不同颜色画笔,

if (hasWon) {

  boxPaint.color = Color(0xff00ff00);

} else {

  boxPaint.color = Color(0xffffffff);

}

如果布尔参数为true,画笔为绿色,否则为白色,然后定义一个点击调用的方法,来根据点击改变布尔参数的值,

void onTapDown() {

  hasWon = !hasWon;

}

box被点中一次布尔参数为true,再点击变为false,这样不同的点击布尔参数就会有不同的状态,这样就会在paint()方法中使用不同颜色的画笔来绘制box。

然后在BoxGame的bool handleEvent(SpriteBoxEvent event) {} 方法中判断点中box时调用onTapDown()方法,替换

//do box actions.

_box.onTapDown();

这样就完成了点击输入方式交互,整体box_game.dart的代码如下,

import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:spritewidget/spritewidget.dart';

class BoxGame extends NodeWithSize {

  Node _gameScreen;
  VirtualJoystick _joystick;

  BoxNode _box;

  double _scroll = 0.0;

  BoxGame() : super(new Size(320.0, 320.0)){

    userInteractionEnabled = true;
    handleMultiplePointers = false;

    _gameScreen = new Node();
    addChild(_gameScreen);

    _joystick = new VirtualJoystick();
    _gameScreen.addChild(_joystick);

    _box = new BoxNode();
    _gameScreen.addChild(_box);

  }

  @override
  bool handleEvent(SpriteBoxEvent event) {

    if (event.type == PointerDownEvent) {

      Offset newPoint = convertPointToNodeSpace(event.boxPosition);

      if(newPoint.dx > _box.boxRect.left &&
          newPoint.dx < _box.boxRect.left + 30 &&
          newPoint.dy > _box.boxRect.top + spriteBox.visibleArea.height &&
          newPoint.dy < _box.boxRect.top + spriteBox.visibleArea.height + 30){
        _box.onTapDown();
      }else{
        return false;
      }
    }
    return true;
  }

  @override
  void spriteBoxPerformedLayout() {
    _gameScreen.position = new Offset(0.0, spriteBox.visibleArea.height);
  }

  void update(double dt) {

    _box.applyThrust(_joystick.value);

  }
}

class BoxNode extends Node {

  bool hasWon = false;
  Rect boxRect;

  BoxNode() {
    position = new Offset(0, 0);
  }

  @override
  void paint(Canvas canvas) {

    boxRect = Rect.fromLTWH(
        spriteBox.visibleArea.height / 2 - 15,
        - spriteBox.visibleArea.height / 2 - 75,
        30,
        30,
    );
   
    Paint boxPaint = Paint();
    boxPaint.color = Color(0xffffffff);

    if (hasWon) {
      boxPaint.color = Color(0xff00ff00);
    } else {
      boxPaint.color = Color(0xffffffff);
    }

    canvas.drawRect(boxRect, boxPaint);
  }

  void onTapDown() {
    hasWon = !hasWon;
  }

  void applyThrust(Offset joystickValue) {

    Offset oldPos = position;
    Offset target = new Offset(joystickValue.dx * 160.0, joystickValue.dy * 220.0);
    double filterFactor = 0.2;

    position = new Offset(
        GameMath.filter(oldPos.dx, target.dx, filterFactor),
        GameMath.filter(oldPos.dy, target.dy, filterFactor));
  }
}

好了你可以体验一下这个游戏了。

关于Node的坐标系统和世界坐标系统的相关联,后期添加。

你可以在Github找到这个练习的完整代码。

你可能感兴趣的:(使用Flutter和SpriteWidget开发第一个跨平台Android/IOS手机游戏 – Beginner Tutorial)