这篇文章让我们来理解下QuickCocos2dx的继承机制,关于继承这个概念,是OO思想里提出来的。在C++中,我们就不说了; 在lua中,是用table配合元表metatable来实现面向对象的。下面分两部分来理解Quick中的继承机制。

##第一部分:LUA的继承

###充电站:元表

  • lua中的元表和js的原型非常相似,熟悉js的朋友应该发现了
  • 在lua中,每一个表都有TA的元表metatable,lua默认创建一个不带元表的新表:
1
2
t = {}
print(getmetatable(t)) --nil
  • 设置元表
1
2
3
4
mt = {name = "quick"}
t = {}
setmetatable(t, mt)
assert(getmetatalbe(t) == mt)

LuaInherit lua继承

图示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
--====================Person======================
local Person = {}
Person.attack = 5

function Person:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end

function Person:setAttack(attack)
self.attack = attack
end

function Person:getAttack()
return self.attack
end


--====================Hero======================
local Hero = Person:new()
Hero.name = ""
Hero.skill = ""


--====================hero1,hero2======================
local hero1 = Hero:new()
hero1.name = "金刚狼"
hero1.skill = "甩开爪子切牛排"

local hero2 = Hero:new({name = "超人"})
hero2.skill = "内裤外穿走T台"


----====================================================
function printKeys(name, t)
print("======================" .. name)
for k, v in pairs(t) do
print(k)
end
end


printKeys("Person", Person)
printKeys("Person.__index", Person.__index)

printKeys("Hero", Hero)
printKeys("Hero.__index", Hero.__index)
printKeys("getmetatable(Hero).__index", getmetatable(Hero).__index)

printKeys("hero1", hero1)

printKeys("hero2", hero2)

结合log我们分析下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
======================Person
setAttack
__index
getAttack
new
attack
======================Person.__index
setAttack
__index
getAttack
new
attack
======================Hero
skill
name
__index
======================Hero.__index
skill
name
__index
======================getmetatable(Hero).__index
setAttack
__index
getAttack
new
attack
======================hero1
name
skill
======================hero2
name
skill
[Finished in 0.0s]

###转回正题

假如当我们调用hero1:setAttack(500)的时候,在hero1中是找不到setAttack方法的,这时候:

  1. lua会通过getmetatable(hero1)得到hero1的元表并到元表的__index域中去查找,箭头走向:3—》2
  2. 但仍然没找到,得到Hero元表并继续在其__index域中寻找,箭头走向:1—》0,这时候寻找到setAttack方法并且调用,由于setAttack方法,hero1会增加字段attack

##第二部分:QUICK的继承

quick的继承实现要考虑到对C++对象的继承和对lua对象的继承。对lua对象的继承我们第一部分已经用元表机制说明。当我们在quick中用class新建类时,始终要清醒的明白,我们新建的类其实就是返回一个lua表(cls).

继承的核心代码见framework/functions.luaclass(classname, super)函数。

为了方便理解,上个图先:

quick-x class

函数class(classname, super)有两个参数:

  • 参数1:classname,见名知意,类名
  • 参数2:super
    • 1.super的类型:superType = type(super)
    • 2.superType可以为function, table,当不为这两种类型的时候我们将之置为nil
    • 3.superType为function的时候,表示从C++对象继承,走图示2
    • 4.superType为table的时候,还要看其__ctype值,1表示继承自C++对象,走图1;2表示继承自lua表对象,走图3
    • 5.superType为nil的时候,从lua表继承,走图4

samples/coinFlip项目是个绝佳的例子,我们可以结合该范例进行理解。这里我就不赘述了。


更新:在谷歌邮件组Jacky的回复中,提到了另外一个朋友写的文章,非常不错,针对本文的第二部分讲解的很透彻,地址


你我是朋友,各拿一个苹果彼此交换,交换后仍然是各有一个苹果;倘若你有一个思想,我也有一种思想,而朋友间交流思想,那我们每个人就有两种思想了。 ——爱尔兰剧作家 萧伯纳

by Waldemar Celes, Ariel Manzur.

tolua++中文参考手册【完整翻译】

官网:http://www.codenix.com/~tolua/tolua++.html
翻 译: ChildhoodAndy
BLOG: childhood.logdown.com
EMAIL:dabing1022@gmail.com

译者前言:
由于工作接触quick-cocos2d-x的缘故,接触到了luabinding。对luabinding的理解在一定程度上会加深对quick框架的理解(也包括cocos2dx的lua绑定),在学习的过程中便涉及到了tolua++。先到官网上大概看了下,有个参考手册,谷歌百度之,现在网络存在的版本有两种,一种是谷歌机器人直接翻译,非常生涩,一种是其他朋友自己翻译的,但不完整,其中的翻译也不是很理想,所以萌生了自己翻译的这么一个想法。我不能保证这个完整版没有错误,但会尽我最大的努力去还原作者的本意。如果读者发现部分的翻译有问题,非常欢迎在我的博客留言或者给我发送email邮件,我会认真阅读,如果正确我将会更新翻译,以便呈现在大家面前的是一个准确描述的译本。


tolua++是tolua的扩展版本,是一款能够集成C/C++与lua代码的工具。在面向C++方面,tolua++包含了一些新的特性比如:

  • 支持std::string作为基本类型(这个可以由一个命令行选项关闭)
  • 支持类模板

以及其他的特性还有一些bug的修复。

tolua这款工具,极大的简化了C/C++代码与lua代码的集成。基于一个干净的头文件(或者从实际头文件中提取), tolua会自动生成从lua访问C/C++功能的绑定代码。

##tolua如何工作

要使用tolua,我们需要创建一个package文件(译者注:pkg文件),即一个从C/C++实际头文件整理后的头文件,列举出我们想导出到lua环境中的那些常量、变量、函数、类以及方法。然后tolua会解析该文件并且创建自动绑定C/C++代码到lua的C/C++文件。如果将创建的文件同我们的应用链接起来,我们就可以从lua中访问指定的C/C++代码。

我们从一些例子开始。如果我们指定下面的类似C头文件作为输入给tolua:

1
#define FALSE 0
#define TRUE 1

enum { 
 POINT = 100, 
 LINE, 
 POLYGON
}
Object* createObejct (int type);
void drawObject (Object* obj, double red, double green, double blue);
int isSelected (Object* obj);

就会自动创建一个绑定上面代码到lua的C文件。因此,在lua代码里, 我们可以访问C代码。举个例子:

1
...
myLine = createObject(LINE)
...
if isSelected(myLine) == TRUE then
  drawObject(myLine, 1.0, 0.0, 0.0);
else
  drawObject(myLine, 1.0, 1.0, 1.0);
end
...

另外,考虑下面类似C++头文件

1
#define FALSE 0
#define TRUE 1
class Shape
{
  void draw (void);
  void draw (double red, double green, double blue);
  int isSelected (void);
};
class Line : public Shape
{
 Line (double x1, double y1, double x2, double y2);
 ~Line (void);
};

如果tolua输入加载该文件,就会自动生成一个C++文件,从而为我们提供lua层访问C++层所需要的对应的代码。因此,以下的的lua代码是有效的:

1
...
myLine = Line:new (0,0,1,1)
...
if myLine:isSelected() == TRUE then
 myLine:draw(1.0,0.0,0.0)
else
 myLine:draw()
end
...
myLine:delete()
...

传给tolua的package文件并不是真正的C/C++头文件,而是清理过的版本。tolua并没有实现对C/C++代码的完全解析,但是却能够解析暴露给lua的功能的声明。通常头文件可以被包括进package文件里,tolua将会提取出用户指定的代码以用于解析头文件。

##如何使用tolua

tolua由两部分代码组成:可执行程序和静态库(an executable and a library)。可执行程序用于解析,读入package文件,然后输出C/C++代码,该代码提供了从lua层访问C/C++层的绑定。如果package文件是与C++类似的代码(例如包括类的定义),就会生成一份C++代码。如果package文件是与C类似的代码(例如不包括类),就会生成一份C代码。tolua可接受一些选项。运行tolua -h显示当前可接受的选项。例如,要解析一个名为myfile.pkg生成一个名为myfile.c的绑定代码,我们需要输入:

tolua -o myfile.c myfile.pkg

产生的代码必须被编译并且和应用程序进行链接,才能提供给Lua进行访问。每个被解析的文件代表导出到lua的package。默认情况下,package的名称就是是输入文件的根名称(例子中为myfile),用户可以指定一个不同的名称给package:

tolua -n pkgname -o myfile.c myfile.pkg

package还应当被明确初始化。为了从C/C++代码中初始化package,我们需要声明和调用初始化函数。初始化函数被定义为:

int tolua_pkgname_open (lua_State*);

其中pkgname是被绑定package的名字。如果我们使用的是C++,我们可以选择自动初始化:

tolua -a -n pkgname -o myfile.c myfile.pkg

在这种情况下,初始化函数会被自动调用。然而,如果我们计划使用多个Lua State,自动初始化就行不通了,因为静态变量初始化的顺序在C++里没有定义。

tolua生成的绑定代码使用了一系列tolua库里面的函数。因此,这个库同样需要被链接到应用程序中。tolua.h也是有必须要编译生成的代码。

应用程序无需绑定任何package文件也可以使用tolua的面向对象框架。在这种情况下,应用程序必须调用tolua初始化函数(此函数被任何package文件初始化函数调用):

int tolua_open (void);

##基本概念

使用tolua的第一步就是创建package文件。我们从真正的头文件入手,将想要暴露给lua的特性转换成tolua可以理解的格式。tolua能够理解的格式就是一些简单的C/C++声明。我们从下面几个方面来讨论:

####文件包含

一个package文件可以包含另外的package文件。一般格式是:

$pfile "include_file"

一个package文件也可以包含常规的C/C++头文件,使用hfile或者cfile命令:

$cfile "example.h"

在这种情况下,tolua将会提取出被tolua_begintolua_end所封闭的代码,或者tolua_export所在的单行。以下面C++代码为例:

1
#ifndef EXAMPLE_H
#define EXAMPLE_H

class Example { // tolua_export

private:

	string name;
	int number;

public:

	void set_number(int number);

	//tolua_begin

	string get_name();
	int get_number();
};
// tolua_end

#endif

在这个例子中,不被tolua支持的代码(类的私有部分),还有set_number函数被留在了package文件之外。

最后,lua文件可以被包含进package文件中,使用$lfile:

$lfile "example.lua"

tolua++新特性:从tolua++1.0.4版本以后,还有一种方式来包含源文件就是用ifile:

$ifile "filename"

ifile允许在文件名之后附带额外的可选的参数,举个例子:

1
$ifile "widget.h", GUI
$ifile "vector.h", math, 3d

ifile默认会将整个文件原封不动的包含进去,但是文件的内容和额外的参数通过include_file_hook函数才能被包含进package文件中。

####基本类型

tolua会自动映射C/C++的基本类型到lua的基本类型。

  • char,int,floatdouble类型被映射为lua中的number
  • char*被映射为lua中的string
  • void*被映射为lua中的userdata

C/C++中的数据类型前面可能有修饰语(如unsigned, static, short, const等等)。然而我们要注意到tolua会忽略基本类型前面的修饰语const。因此,我们给lua传递一个基本类型常量然后再从lua中传递回给C/C++代码,常量到非常量的转换会被悄悄的完成。

C/C++函数也可以对lua对象进行明确的操作。lua_Object被认为是一个基本类型,任何lua值都符合。

tolua++新特性 :C++中的string类型同样被认为是基本类型,会被当作值传递给lua(使用c_str()方法)。这个功能可以使用命令行选项-S进行关闭。

####用户自定义类型

在package文件里的所有其他类型都会被认为是用户自定义类型。它们会映射到lua的userdata类型。lua只能存储指向用户自定义类型的指针;但是,tolua会自动采取措施来处理引用和值。例如,如果一个函数或方法返回一个用户定义类型的值,当这个值返回给lua的时候,tolua会分配一个克隆对象,同时会设置垃圾收集标记,以便在lua不再使用该对象时会自动释放。

对于用户定义类型,常量是被保留的,因此将用户自定义类型的非常量作为常量传递给一个函数时,会产生类型不匹配的错误。

####NULL和nil

C/C++的NULL或0指针映射到lua中的nil类型。反之,nil却可以被指定为任何C/C++指针类型。这对任何类型都有效:char*, void*以及用户自定义类型指针。

####Typedefs

#####真实头文件的包含

##绑定常量

tolua支持两种绑定常量的方式:defineenum

  • define通常的格式是:#define NAME [ VALUE ]

上面的VALUE是可选的。如果这样的代码出现在被解析的文件中,tolua会将NAME作为lua的全局变量,该全局变量是C/C++的常量,值为VALUE。这里只接受数字常量。

tolua++新特性:所有的其他预处理指令会被忽略。

  • enum的一般格式:
1
enum {
  NAME1 [ = VALUE1 ] ,
  NAME2 [ = VALUE2 ] ,
  ...
  NAMEn [ = VALUEn ]
};

同样的,tolua会创建一系列全局变量,命名为NAMEi,对应着各自的值。

##绑定外部变量

##tolua++的额外特性

####多个变量声明
同一类型的多个变量可以一次性声明,如:

float x,y,z;

这将会创建3个不同的浮点型变量。确保逗号之间没有任何空格,否则会解析错误。

####tolua_readonly
任何变量声明都可以使用tolua_readonly修饰符来确保变量只读,即使该变量为非常量类型。例如:

1
class Widget {

	tolua_readonly string name;
};

这个特性可以用来’hack’一些不支持的东西如operator->。考虑下面这个pkg文件:

1
$hfile "node.h"
$#define __operator_arrow operator->()
$#define __get_name get_name()

node.h文件内容如下:

1
template class<T>
class Node { // tolua_export

private:
	string name;
	T* value;

public:

	T* operator->() {return value;};
	string get_name() {return name;};

	// tolua_begin

	#if 0
	TOLUA_TEMPLATE_BIND(T, Vector3D)

	tolua_readonly __operator_arrow @ p;
	tolua_readonly __get_name @ name;
	#endif


	Node* next;
	Node* prev;

	void set_name(string p_name) {name = p_name;};

	Node();
};
// tolua_end

虽然这样对头文件的处理不是很漂亮,但是却解决了下面几个问题:

  • operator->()方法可以通过在lua中使用对象的变量p来调用
  • get_name()方法可以通过在lua中使用对象的变量name来调用

lua用法举例:

1
node = Node_Vector3D_:new_local()
-- do something with the node here --
print("node name is "..node.name)
print("node value is ".. node.p.x ..", ".. node.p.y ..", ".. node.p.z)

tolua++会忽略掉所有的预处理命令(除了#define), 但node.h仍然是一个有效的C++头文件,同时也是tolua的一个有效的源,这样就避免了维护2个不同的文件,即使是具有不一样的功能的对象。

像变量一样去重命名函数的功能特性可能会被扩展进将来的版本中。

####在命令行中定义值

从版本1.0.92开始,命令行选项-E在tolua++运行的时候允许我们传值给luaState,就像GCC的-D一样。例如:

1
$ tolua++ -E VERSION=5.1 -E HAVE_ZLIB package.pkg > package_bind.cpp

上述命令将会在全局表_extra_parameters中添加两个字段,字符串值为“5.1”的“VERSION”,还有布尔值为true的“HAVE_ZLIB”。就目前而言,我们还不能使用这些值,除非用户自定义脚本。

####使用C++ typeid

从版本1.0.92开始,命令行选项-t变得可用。这将会产生一个对空宏Mtolua_typeid的调用的列表,该列表包含了C++ type_infoobject和区分其他类型的名字。例如,如果你的package文件中绑定了2个类,FooBar,使用-t会有如下输出:

1
#ifndef Mtolua_typeid
#define Mtolua_typeid(L,TI,T)
#endif
 Mtolua_typeid(tolua_S,typeid(Foo), "Foo");
 Mtolua_typeid(tolua_S,typeid(Bar), "Bar");

Mtolua_typename的实现留给读者作为练习。

##导出实用函数

tolua本身为lua导出了一些实用的函数,包括了面向对象的框架。tolua使用的package文件如下:

1
module tolua 
{ 
 char* tolua_bnd_type @ type (lua_Object lo); 
 void tolua_bnd_takeownership @ takeownership (lua_Object lo); 
 void tolua_bnd_releaseownership @ releaseownership (lua_Object lo); 
 lua_Object tolua_bnd_cast @ cast (lua_Object lo, char* type); 
 void tolua_bnd_inherit @ inherit (lua_Object table, lua_Object instance);

/* for lua 5.1 */ 
 void tolua_bnd_setpeer @ setpeer (lua_Object object, lua_Object peer_table); 
 void tolua_bnd_getpeer @ getpeer (lua_Object object); 
}

####tolua.type(var)

返回一个代表对象类型的字符串。例如,tolua.type(tolua)返回字符串table, tolua.type(tolua.type)返回cfunction。类似的,如果var是一个存储着用户自定义类型T的变量,tolua.type(var)将会返回const T或者T, 取决于它是否是一个常量引用。

#####tolua.takeownership(var)

接管对象引用变量的所有权,意味着当对象的所有引用都没有的话,对象本身将会被lua删除。

#####tolua.releaseownership(var)

释放对象引用变量的所有权。

#####tolua.cast(var, type)

改变var的元表以便使得它成为type类型。type须是一个完整的C类型对象(包括命名空间等)字符串。

#####tolua.inherit(table, var)

(tolua++新特性)tolua++会将table作为对象和var具有一样的类型,当必要的时候使用var。举个例子,考虑下这个方法:

1
void set_parent(Widget* p_parent);

一个lua对象可以这么使用:

1
local w = Widget()
local lua_widget = {}
tolua.inherit(lua_widget, w)

set_parent(lua_widget);

注意这只是使得table能够在必要的时候被识别为Widget类型。为了能够访问Widget的方法,你必须要实现你自己的对象系统。下面是个简单的例子:

1
lua_widget.show = Widget.show

lua_widget:show() 
-- 调用Widget的show方法
-- table作为self,因为 lua_widget继承于一个widget实例
-- tolua++会把self识别为Widget,并且调用方法

当然,一个更好的办法就是为lua对象增加__index元方法。

##嵌入Lua代码

tolua允许我们将lua代码嵌入到生成的C/C++代码中。要做到这一点,它对指定的lua代码进行编译,并且在生成的代码中产生一个C常量字符串,存储下来相应的字节码。当package文件被打开的时候,字符串被执行。嵌入的lua代码格式如下:

1
$[

嵌入的lua代码
...

$]

以下面的.pkg摘录为例:

1
/* Bind a Point class */ 
class Point 
{ 
 Point (int x, int y); 
 ~Point (); 
 void print (); 
 ... 
} CPoint;

$[

-- Create a Point constructor 
function Point (self) 
 local cobj = CPoint:new(self.x or 0, self.y or 0) 
 tolua.takeownership(cobj) 
 return cobj 
end

$]

绑定这样的代码之后,我们可以在lua中这么写:

1
p = Point{ x=2, y=3 } 
p:print() 
...


# 目录

目录

注:写这篇文章的时候,笔者所用的是quick-cocos2d-x 2.2.1rc版本。

## quick状态机
状态机的设计,目的就是为了避免大量状态的判断带来的复杂性,消除庞大的条件分支语句,因为大量的分支判断会使得程序难以修改和扩展。但quick状态机的设计又不同设计模式的状态模式,TA没有将各个状态单独划分成单独的状态类,相反根据js、lua语言的特点,特别设计了写法,使用起来也比较方便。

quick框架中的状态机,是根据javascript-state-machine重新设计改写而成,同时sample/statemachine范例也是根据js版demo改写而来。该js库现在是2.2.0版本。基于js版的README.md,结合廖大的lua版重构,我针对状态机的使用做了点说明,如果有不对的地方,感谢指出:)。

推荐大家在理解的时候结合sample/statemachine范例进行理解,注意player设置成竖屏模式,demo里面的按钮在横屏模式下看不见。

# sample图示
StateMachine


# 用法
创建一个状态机

1
2
3
4
5
6
7
8
9
10
11
local fsm = StateMachine.new()
-- (注:和demo不同的是,demo采用组件形式完成的初始化)
fsm:setupState({
initial = "green",
events = {
{name = "warn", from = "green", to = "yellow"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "yellow"},
{name = "clear", from = "yellow", to = "green" },
}
})

之后我们就可以通过

  • fsm:doEvent(“start”)-从”none”状态转换到”green”状态
  • fsm:doEvent(“warn”)-从”green”状态转换到”yellow”状态
  • fsm:doEvent(“panic”)-从”green”状态转换到”red”状态
  • fsm:doEvent(“calm”)-从”red”状态转换到”yellow”状态
  • fsm:doEvent(“clear”)-从”yellow”状态转换到”green”状态

同时,

  • fsm:isReady()-返回状态机是否就绪
  • fsm:getState()-返回当前状态
  • fsm:isState(state)-判断当前状态是否是参数state状态
  • fsm:canDoEvent(eventName)-当前状态如果能完成eventName对应的event的状态转换,则返回true
  • fsm:cannotDoEvent(eventName)-当前状态如果不能完成eventName对应的event的状态转换,则返回true
  • fsm:isFinishedState()-当前状态如果是最终状态,则返回true
  • fsm:doEventForce(name, …)-强制对当前状态进行转换

# 单一事件的多重from和to状态
如果一个事件允许我们从多个状态(from)转换到同一个状态(to), 我们可以通过用一个集合来构建from状态。如下面的”rest”事件。但是,如果一个事件允许我们从多个状态(from)转换到对应的不同的状态(to),那么我们必须将该事件分开写,如下面的”eat”事件。

1
2
3
4
5
6
7
8
9
10
local fsm = StateMachine.new()
fsm:setupState({
initial = "hungry",
events = {
{name = "eat", from = "hungry", to = "satisfied"},
{name = "eat", from = "satisfied", to = "full"},
{name = "eat", from = "full", to = "sick" },
{name = "rest", from = {"hungry", "satisfied", "full", "sick"}, to = "hungry"},
}
})

在设置了事件events之后,我们可以通过下面两个方法来完成状态转换。

  • fsm:doEvent(“eat”)
  • fsm:doEvent(“rest”)

rest事件的目的状态永远是hungry状态,而eat事件的目的状态取决于当前所处的状态。

注意1:如果事件可以从任何当前状态开始进行转换,那么我们可以用一个通配符*来替代from状态。如rest事件,我们可以写成{name = "rest", from = "*", to = "hungry"}

注意2:上面例子的rest事件可以拆分写成4个,如下:

1
{name = "rest", from = "hungry",    to = "hungry"},
{name = "rest", from = "satisfied", to = "hungry"},
{name = "rest", from = "full",      to = "hungry"},
{name = "rest", from = "sick",      to = "hungry"}


# 回调
quick的状态机支持4种特定事件类型的回调:

  • onbeforeEVNET- 在特定事件EVENT开始前被激活
  • onleaveSTATE - 在离开旧状态STATE时被激活
  • onenterSTATE - 在进入新状态STATE时被激活
  • onafterEVENT - 在特定事件EVENT结束后被激活

注解:编码时候,EVENT/STATE应该被替换为特定的名字

为了便利起见,

  • onenterSTATE可以简写为onSTATE
  • onafterEVENT可以简写为onEVENT

所以假如要使用简写的话,为了避免onSTATEonEVENT的STATE/EVENT被替换成具体的名字后名字相同引起问题,to状态和name名字尽量不要相同。比如

1
2
3
4
5
6
7
8
9
-- 角色开火
{name = "fire", from = "idle", to = "fire"}
--假如使用简写
--onSTATE --- onfire
--onEVENT --- onfire,回调会引起歧义。

--如果不使用简写
--则onenterSTATE --- onenterfire
--onafterEVENT --- onafterfire

另外,我们可以使用5种通用型的回调来捕获所有事件和状态的变化:

  • onbeforeevent- 在任何事件开始前被激活
  • onleavestate - 在离开任何状态时被激活
  • onenterstate - 在进入任何状态时被激活
  • onafterevent - 在任何事件结束后被激活
  • onchangestate - 当状态发生改变的时候被激活

注解:这里是任何事件、状态, 小写的event、state不能用具体的事件、状态名字替换。

###回调参数
所有的回调都以event为参数,该event为表结构,包含了

  • name 事件名字
  • from 事件表示的起始状态
  • to 事件表示的目的状态
  • args 额外的参数,用来传递用户自定义的一些变量值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local fsm = StateMachine.new()
fsm = fsm:setupState({
initial = "green",
events = {
{name = "warn", from = "green", to = "yellow"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "yellow"},
{name = "clear", from = "yellow", to = "green" },
},
callbacks = {
onbeforestart = function(event) print("[FSM] STARTING UP") end,
onstart = function(event) print("[FSM] READY") end,
onbeforewarn = function(event) print("[FSM] START EVENT: warn!") end,
onbeforepanic = function(event) print("[FSM] START EVENT: panic!") end,
onbeforecalm = function(event) print("[FSM] START EVENT: calm!") end,
onbeforeclear = function(event) print("[FSM] START EVENT: clear!") end,
onwarn = function(event) print("[FSM] FINISH EVENT: warn!") end,
})
fsm:doEvent("warn", "some msg")

如上例子,fsm:doEvent("warn", "some msg")中的some msg作为额外的参数字段args结合name from to被添加到event,此时

1
2
3
4
5
6
event = {
name = "warn",
from = "green",
to = "yellow",
args = "some msg"
}

event表正是回调函数的参数。

###回调顺序
用{name = “clear”, from = “red”, to = “green”}举例,我画个示意图来说明
callback

注意:之前的onbeforeEVENT,这里EVENT就被具体替换为clear,于是是onbeforeclear,而onbeforeevent类似的通用型则不用替换。

  • onbeforeclear - clear事件执行前的回调
  • onbeforeevent - 任何事件执行前的回调
  • onleavered - 离开红色状态时的回调
  • onleavestate - 离开任何状态时的回调
  • onentergreen - 进入绿色状态时的回调
  • onenterstate - 进入任何状态时的回调
  • onafterclear - clear事件完成之后的回调
  • onafterevent - 任何事件完成之后的回调

####3种影响事件响应的方式

  1. onbeforeEVENT方法中返回false来取消事件
  2. onleaveSTATE方法中返回false来取消事件
  3. onleaveSTATE方法中返回ASYNC来执行异步状态转换

# 异步状态转换
有时候,我们需要在状态转换的时候执行一些异步性代码来确保不会进入新状态直到代码执行完毕。
举个例子来说,假如要从一个menu状态转换出来,或许我们想让TA淡出?滑出屏幕之外?总之执行完动画再进入game状态。

我们可以在onleavestate或者onleaveSTATE方法里返回StateMachine.ASYNC,这时状态机会被挂起,直到我们使用了event的transition()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
onleavered = function(event)
self:log("[FSM] LEAVE STATE: red")
self:pending(event, 3)
self:performWithDelay(function()
self:pending(event, 2)
self:performWithDelay(function()
self:pending(event, 1)
self:performWithDelay(function()
self.pendingLabel_:setString("")
event.transition()
end, 1)
end, 1)
end, 1)
return "async"
end,
...

提示:如果想取消异步事件,可以使用event的cancel()方法。


# 初始化选项

  • 状态机的初始化选项一般根据我们游戏需求来决定,quick状态机提供了几个简单的选项。
    在默认情况下,如果你没指定initial状态,状态机会指定当前状态为none状态,所以需要定义一个能将none状态转换出去的事件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
    events = {
    {name = "startup", from = "none", to = "green" },
    {name = "panic", from = "green", to = "red" },
    {name = "calm", from = "red", to = "green"}
    }
    })
    echoInfo(fsm:getState()) -- "none"
    fsm:doEvent("start")
    echoInfo(fsm:getState()) -- "green"
  • 如果我们特别指定了initial状态,那么状态机在初始化的时候会自动创建startup事件,并且被执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
    initial = "green",
    events = {
    -- 当指定initial状态时,这个startup事件会被自动创建,所以可以不用写这一句 {name = "startup", from = "none", to = "green" },
    {name = "panic", from = "green", to = "red" },
    {name = "calm", from = "red", to = "green"}
    }
    })
    echoInfo(fsm:getState()) -- "green"
  • 我们也可以这样指定initial状态:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
    initial = {state = "green", event = "init"},
    events = {
    {name = "panic", from = "green", to = "red" },
    {name = "calm", from = "red", to = "yellow"}
    }
    })
    echoInfo(fsm:getState()) -- "green"
  • 如果我们想延缓初始化状态转换事件的执行,我们可以添加defer = true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    local fsm = StateMachine.new()
    fsm = fsm:setupState({
    initial = {state = "green", event = "init", defer = true},
    events = {
    {name = "panic", from = "green", to = "red" },
    {name = "calm", from = "red", to = "green"}
    }
    })
    echoInfo(fsm:getState()) -- "none"
    fsm:doEvent("init")
    echoInfo(fsm:getState()) -- "green"

# 异常处理
在默认情况下,如果我们尝试着执行一个当前状态不允许转换的事件,状态机会抛出异常。如果选择处理这个异常,我们可以定义一个错误事件处理。在quick中,发生异常的时候StateMachine:onError_(event, error, message)会被调用。

1
2
3
4
5
6
7
8
9
10
11
local fsm = StateMachine.new()
fsm:setupState({
initial = "green",
events = {
{name = "warn", from = "green", to = "yellow"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "green"},
{name = "clear", from = "yellow", to = "green" },
}
})
fsm:doEvent("calm") -- fsm:onError_会被调用,在当前green状态下不允许执行calm事件


本文如果有写的不对的地方,还请大家指出,交流学习:)
如果朋友们有关于状态机的使用心得,也非常欢迎分享。

###QuickCocos2dx创建描边效果###

@[quick|cocos2d|描边]


效果

参考:


CCRenderTexture这个类平时没怎么用过,想不到用TA来创建描边效果轻松并且效果也还不错。

上面2个帖子的实现,抛开一个c++,一个oc语言来看,我发现思路其实是一样的,这里我翻译了成quick的lua版本,并在代码中相应做了些注释,朋友们可以看下。

描边函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
-- @param:node 欲描边的显示对象
-- @param:strokeWidth 描边宽度
-- @param:color 描边颜色
-- @param:opacity 描边透明度
function createStroke(node, strokeWidth, color, opacity)
local w = node:getTexture():getContentSize().width + strokeWidth * 2
local h = node:getTexture():getContentSize().height + strokeWidth * 2
local rt = CCRenderTexture:create(w, h)

-- 记录原始位置
local originX, originY = node:getPosition()
-- 记录原始颜色RGB信息
local originColorR = node:getColor().r
local originColorG = node:getColor().g
local originColorB = node:getColor().b
-- 记录原始透明度信息
local originOpacity = node:getOpacity()
-- 记录原始是否显示
local originVisibility = node:isVisible()
-- 记录原始混合模式
local originBlend = node:getBlendFunc()

-- 设置颜色、透明度、显示
node:setColor(color)
node:setOpacity(opacity)
node:setVisible(true)
-- 设置新的混合模式
local blendFuc = ccBlendFunc:new()
blendFuc.src = GL_SRC_ALPHA
blendFuc.dst = GL_ONE
-- blendFuc.dst = GL_ONE_MINUS_SRC_COLOR
node:setBlendFunc(blendFuc)

-- 这里考虑到锚点的位置,如果锚点刚好在中心处,代码可能会更好理解点
local bottomLeftX = node:getTexture():getContentSize().width * node:getAnchorPoint().x + strokeWidth
local bottomLeftY = node:getTexture():getContentSize().height * node:getAnchorPoint().y + strokeWidth

local positionOffsetX = node:getTexture():getContentSize().width * node:getAnchorPoint().x - node:getTexture():getContentSize().width / 2
local positionOffsetY = node:getTexture():getContentSize().height * node:getAnchorPoint().y - node:getTexture():getContentSize().height / 2

local rtPosition = ccp(originX - positionOffsetX, originY - positionOffsetY)

rt:begin()
-- 步进值这里为10,不同的步进值描边的精细度也不同
for i = 0, 360, 10 do
-- 这里解释了为什么要保存原来的初始信息
node:setPosition(ccp(bottomLeftX + math.sin(degrees2radians(i)) * strokeWidth, bottomLeftY + math.cos(degrees2radians(i)) * strokeWidth))
node:visit()
end
rt:endToLua()

-- 恢复原状
node:setPosition(originX, originY)
node:setColor(ccc3(originColorR, originColorG, originColorB))
node:setBlendFunc(originBlend)
node:setVisible(originVisibility)
node:setOpacity(originOpacity)

rt:setPosition(rtPosition)

return rt
end

###补充###

弧度与角度转换函数
1
2
3
4
5
6
7
function degrees2radians(angle)
return angle * 0.01745329252
end

function radians2degrees(angle)
return angle * 57.29577951
end

###举个例子###

```lua 测试例子
– 文本、图片一样,这里用文本举个例子
local quickLabel = ui.newTTFLabel({
text = “QuickCocos2dX-createStroke”,
color = display.COLOR_RED,
size = 60,
align = ui.TEXT_ALIGN_CENTER,
x = display.cx,
y = display.cy + 150
}):addTo(self, 1)

local renderTexture = createStroke(quickLabel, 4, ccc3(0xca, 0xa5, 0x5f), 100)
– 设置反锯齿
renderTexture:getSprite():getTexture():setAntiAliasTexParameters()
self:addChild(renderTexture, quickLabel:getZOrder()-1)
```
这样的方法,会drawcall2次。

群里有朋友提到,如果文本改变,那下方的描边如何做?

那对应的描边renderTexture也需要改变,我想是得先remove掉然后重新创建纹理,那各位可以简单封装下,当文本重新setString的时候,相应去创建下方的描边纹理就可以了。

###最后###
如果代码有问题或者其他疑问,欢迎一起探讨学习:)

@[Cocos2d|纹理]

###写在前面###
在cocos2d中,纹理与图形渲染有着密不可分的关系。纹理会存储在GPU内存中,纹理的像素格式决定了图片所占用的GPU内存,跟加载的所用的时间没有关系,但可以通过减小PNG/GIF/TIFF/TMP等图片的大小来缩短加载的时间,也可以将图像格式导出成PVR格式,IOS设备通常使用PowerVR显示芯片,而PVR格式的图片可以直接被该显示芯片直接读取,不需要解析直接显示,渲染速度更快,更节省内存,pvr.ccz图像格式是pvr的压缩格式,占用空间更小。

TexturePacker中所列举的纹理像素格式

我们可能用到像素格式有:

  • BGRA8888(32 bit)

32位真彩色,红、绿、蓝、透明通道各8位,占用内存是16位纹理的2倍,整个场景的背景图片、具有大量渐变色的图片可以考虑这个格式

  • RGBA8888(32 bit)

同上

  • RGB888(24 bit)

24位真彩色,红、绿、蓝通道各8位,没有透明通道

  • RGB565(16 bit)

16位,红、蓝通道5位,绿色通道6位,是一个图像质量还不错的16位纹理格式,也可考虑用作游戏中的背景图像

  • A8(8 bit)

只有透明通道的8位,用来做遮罩图

  • I8(8 bit)

8位,存储灰度信息,用来做灰度图

  • AI88(16 bit)

16位,存储透明度和灰度信息

  • RGBA4444(16 bit)
  • RGB5A1(16 bit)

16位,红、绿、蓝通道各5位,透明通道1位(0/1),所以透明度的表现只有两种,非黑即白

  • PVRTC4(4 bit)
  • PVRTC4A(4 bit)
  • PVRTC2(2 bit)
  • PVRTC2A(2 bit)
  • ETC
  • S3TC_DXT1
  • S3TC_DXT3
  • S3TC_DXT5
  • ATC_RGB
  • ATC_EXPLICIT_ALPHA
  • ATC_INTERPOLATED_ALPHA

###怎么使用###
Cocos2dX版本:3.0

在载入图像之前设置纹理格式,然后进行精灵实例化操作

1
2
Texture2D::setDefaultAlphaPixelFormat(Texture2D::PixelFormat::RGBA8888);
Sprite* sprite = Sprite::create("test.png");

当纹理被创建之后,我们就不能再修改纹理的像素格式了,但是我们可以通过CCTextureCache移除该纹理,就可以重新从同个图片资源用不同的纹理像素格式来创建纹理了。

###相关阅读###

本文针对版本:2.2.1rc版或者之前的版本,最新2.2.3版本廖大对触摸做了不少改动,具体的触摸改动可以查看samples里的touch测试例子

刚开始接触quick,在用的过程中,对于触摸以及吞噬的使用有些问题。在浏览谷歌邮件组的时候,发现廖大对quick的触摸事件流作了一些说明。

保留了 cocos2dx 已有的触摸事件处理机制,扩展了:

  1. 任意 Node 都可以响应触摸事件,并且按照该 Node 及其所有子 Node 占用的屏幕空间来判断触摸响应区域;
  2. 按照 Node 的渲染次序来响应触摸,也就是说最上层的 Node 最先响应触摸,用来制作对话框就很方便了。
  3. 在 Lua 框架里提供了多种按钮类型。

###注意###

  • cocos2d-x 的触摸是基于 CCLayer 的。而 CCLayer 按照优先级来响应触摸。
  • quick 的触摸是基于显示层级的。

###2014/1/14更新###

####版本:QuickCocos2dX 2.2.1rc

举个例子:)

模态层示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- 场景中的按钮
local leftMenu = cc.ui.UIPushButton.new("somePic.png")
:onButtonPressed(function(event)
print("left menu pressed")
end)
:onButtonRelease(function(event)
print("left menu release")
end)
:onButtonClicked(function(event)
print("left menu clicked")
end)
:align(display.CENTER, display.cx - 100, display.cy)
:addTo(self)

-- 模态层
local poplayer = display.newSprite():addTo(self)
poplayer:setCascadeBoundingBox(CCRectMake(0, 0, display.width, display.height))
poplayer:setTouchEnabled(true)
poplayer:addTouchEventListener(function(event, x, y)
if event == "began" then
return true
elseif event == "ended" then
print("pop layer touch ended......")
end

end)

-- 模态层中的按钮
local rightMenu = cc.ui.UIPushButton.new("somePic.png")
:onButtonPressed(function(event)
print("right menu pressed")
end)
:onButtonRelease(function(event)
print("right menu release")
end)
:onButtonClicked(function(event)
print("right menu clicked")
end)
:align(display.CENTER, display.cx + 100, display.cy)
:addTo(poplayer)

可以将上面代码拷贝到工程中进行测试,将图片somePic.png换成你自己的按钮图片,测试发现,leftMenu位于poplayer之下,触摸是不响应的,poplayer中的rightMenu能够响应。

##Cocos2dX中CCTableView使用中遇到的一个问题##

最近在做一个游戏原型,其中用到了CCTableView,在使用的过程中遇到了一个疑问。
为了方便说明问题,我用了Cocos2dx项目中的cocos2d-x-2.2/samples/Cpp/TestCpp测试例子中的ExtensionsTestTableViewTest来举例。

问题描述:当点击其中一个cell的时候,我为了记录我点击是哪个cell,我将该cell下的的背景图(tag设置为了100)设置显示为红色,当我点击了index为0也就是第一个cell的时候,第一个被设置成了红色,但是当我滑动这个tableView的时候,发现还有其他cell也被变成了红色。如下图所示。

1

2

3

4

##我的做法是这样的,不知道有啥问题,为什么会出现这个现象?##

修改tableCellAtIndex函数,增加一行代码:`sprite->setTag(100)`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CCTableViewCell* TableViewTestLayer::tableCellAtIndex(CCTableView *table, unsigned int idx)
{
CCString *string = CCString::createWithFormat("%d", idx);
CCTableViewCell *cell = table->dequeueCell();
if (!cell) {
cell = new CustomTableViewCell();
cell->autorelease();
CCSprite *sprite = CCSprite::create("Images/Icon.png");
sprite->setAnchorPoint(CCPointZero);
sprite->setPosition(ccp(0, 0));
sprite->setTag(100);
cell->addChild(sprite);
CCLabelTTF *label = CCLabelTTF::create(string->getCString(), "Helvetica", 20.0);
label->setPosition(CCPointZero);
label->setAnchorPoint(CCPointZero);
label->setTag(123);
cell->addChild(label);
}
else
{
CCLabelTTF *label = (CCLabelTTF*)cell->getChildByTag(123);
label->setString(string->getCString());
}
return cell;
}
修改tableCellTouched函数
1
2
3
4
5
6
7
void TableViewTestLayer::tableCellTouched(CCTableView* table, CCTableViewCell* cell)
{
CCLOG("cell touched at index: %i", cell->getIdx());

CCSprite* sprite = (CCSprite*)cell->getChildByTag(100);
sprite->setColor(ccc3(255, 0, 0));
}

Cocos2d-iPhone调用lua

@[Cocos2d-iPhone|lua|wax|libLua.a]

  • 安装cocos2d-iPhone模板到Xcode下,这个这里就不说了。
  • 我新建了一个cocos2d项目,名称为Cocos2d-iPhoneLuaTest,在Xcode界面下的左侧选中项目,然后在右边界面点击下方的”AddTarget”。
    Alt text
  • 在弹出的对话框中创建静态库,如下图所示
    Alt text
  • 然后点击下一步(next),在对话框中填写加入的静态库名字,确定。
  • 在Xcode界面下的左侧选中项目,右键加入外部文件
  • 在弹出的对话框中选择解压好的lua,我用的是lua-5.2.2,注意要将其加到Lua目标中
    Alt text
  • 如下图所示,要将静态库libLua.a加入到链接库中
    Alt text
  • 准备工作完毕。

简单的编码实现

  • 将HelloWorldLayer.m文件后缀改成mm,支持Objective-C和C++混编。
  • 在HelloWorldLayer.mm文件中加入如下代码

    extern "C"
    {
            #include "lua.h"
            #include "lualib.h"
            #include "lauxlib.h"
    };
    
    int runlua(void)
    {
                lua_State *L = luaL_newstate();
            luaL_openlibs(L);
            printf("\nLua done!");
            lua_close(L);
            return 0;
    }
    
  • 在HelloWorldLayer.mm的init方法里面,添加对runLua方法的调用

    -(id) init
    {
        if( (self=[super init]) ) {
              CCLOG(@"HelloWorldLayer init.");
    
              runlua();
        }
        return self;
    }
    
  • ok,run,日志里”Lua done!”

最后说下,cocos2d-iPhone没有内置lua,不过有个开源的lua实现框架wax,《愤怒的小鸟》正是采用了wax。cocos2d-x集成了lua,而且最新的版本也集成了jsbinding,十分强大,有兴趣的可以自行查找研究学习。

在cocos2d中如何重新加载当前场景?

1
CCScene *currentScene = [[CCDirector sharedDirector]runningScene];
[[CCDirector sharedDirector]replaceScene:currentScene];

上面的方法肯定不对啦
更改如下:

1
[[CCDirector sharedDirector] replaceScene:[YourSceneClass scene]];

或者:

1
CCScene *currentScene = [CCDirector sharedDirector].runningScene;
CCScene *newScene = [[[currentScene class] alloc] init];
[[CCDirector sharedDirector] replaceScene:newScene];

最近因为工作原因,需要学习下lua,由于cocos2d-x很好的整合了lua,实现app热更新很容易。
在看lua资料的时候,接触到了另外一个优秀的开源引擎Love2d,love2d是个小巧的lua开源引擎,社区氛围非常良好。从论坛上看的出来,国外不少游戏独立制作人在使用love2d,从youtube上的视频也能看出,love2d虽然小巧,但五脏俱全,制作的游戏丝毫不逊色。Cocos2d和Love2d都是开源的,我们能从开源的产品中学习到很多东西,比如如何去架构一个引擎,他们是如何对游戏世界里面的物体、事件、动作等进行抽象的,里面的设计思路和代码包括设计模式都有很多值得我们去学习的地方。
这些天好好准备拿下这块骨头。