基于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() .
然后删除./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找到这个练习的完整代码。