OPhone平台2D游戏引擎实现——场景、图层、元素(一)
2012-09-07 11:27
513 查看
上一篇我们对游戏引擎的框架进行了分析,同时也完成了一个基础的游戏引擎框架,那么这一篇我们将完成游戏引擎中的场景,图层,元素(节点)进行实现,但是在开始之前,我们可以对上一篇所介绍的框架进行完善,首先我们可以对引擎的渲染窗口(YFSGLSurfaceView)进行完善,使其具备事件处理能力和对渲染器(Renderer)进行控制。
首先在YFSGLSurfaceView中声明以下成员:
view plaincopy
to clipboardprint?
//渲染器
private Director mDirector;
//事件调度器
private EventDispatcher mDispatcher;
然后,需要在构造函数中实例化这些对象,代码如下:
//取得Director
this.mDirector = Director.getInstance();
//取得context
this.mDirector.context = context;
//取得EventDispatcher
this.mDispatcher = EventDispatcher.getInstance();
同时在退出函数surfaceDestroyed中,我们将调用Director的onSurfaceDestroyed函数来处理退出操作,释放资源等,代码如下:
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
this.mDirector.onSurfaceDestroyed();
}
最后就是事件处理部分,由于我们对事件的处理都是在EventDispatcher中进行统一管理调度的,所以我们只需要按照不同的事件类型,将事件传递给EventDispatcher即可,EventDispatcher便会为我们进行事件处理,如代码清单2-1所示。
代码清单2-1:事件处理
view plaincopy
to clipboardprint?
//触笔事件处理
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
this.mDispatcher.touchesCancelled(event);
break;
case MotionEvent.ACTION_DOWN:
this.mDispatcher.touchesBegan(event);
break;
case MotionEvent.ACTION_MOVE:
this.mDispatcher.touchesMoved(event);
break;
case MotionEvent.ACTION_UP:
this.mDispatcher.touchesEnded(event);
}
return true;
}
//按键事件处理
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (this.mDispatcher.keyDown(event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (this.mDispatcher.keyUp(event)) {
return true;
}
return super.onKeyUp(keyCode, event);
}
好了,上面则是我们对上一篇引擎的框架进行完善,当然还有Director需要完善,我们会在后面组件进行完善。这里我们先来分析最基础的场景元素,即前面说的节点。
场景元素
从名字大家可以看出来,所谓的场景元素,那么肯定就是能够放置在场景中的任何对象,比如一棵树、一个怪物、一个按钮,一个标签等都是场景中的元素,甚至场景、图层本身也是一个场景元素,即节点。我们将在后面给大家分析,大家也可以跳到后面去看一下,这样更有助于理解场景元素的定义。
节点基类实现
我们可以为这所有的元素抽象一个基类Node,任何可以在场景中放置的元素都将继承自该类。Node封装了各种元素的的通用逻辑和渲染过程,这里我们也把这些元素称之为“节点”,在后文中出现的节点就表示元素。首先我们要确定一个通用节点需要哪些属性?如代码清单2-2所示。
代码清单2-2:节点的部分属性
view plaincopy
to clipboardprint?
public class Node {
protected static final int INVALID_TAG = -1;
//锚点比例
protected PointF mAnchorPercent;
//摄像头
private Camera mCamera;
//子节点
protected ArrayList<Node> mChildren;
//尺寸大小
protected YFSSize mContentSize;
private BaseGrid mGrid;
//是否运行中...
private boolean mRunning;
//变换
protected boolean mTransformDirty;
protected boolean mInverseDirty;
//父节点
private Node mParent;
//位置
protected YFSPoint mPosition;
//锚点是否发生作用
private boolean mRelativeAnchorPoint;
//是否显示
private boolean mEnabled;
//是否选中
private boolean mSelected;
//旋转,缩放
private float mRotation;
private float mScaleX;
private float mScaleY;
//tag标记
protected int mTag;
//放射变换
private YFSAffineTransform mTransformMatrix;
private YFSAffineTransform mInverseMatrix;
//锚点位置
protected YFSPoint mAnchorPosition;
//其他附加数据
private Object mUserData;
//节点在OpenGL中的z order值
private float mVertexZ;
//是否可见
protected boolean mVisible;
//节点在父节点中的z order值
private int mZOrder;
//...
}
代码清单2-2列出了一个通用节点所需要的常用属性,另外每个节点还可以设置其动画,我们将在引擎的动画部分完成之后在来完善节点的动画,大家可以根据注解来理解,这里我们主要说明一下“锚点”,我们对每一个节点都设置了一个锚点,比如一个节点的大小尺寸为480*320,锚点被设置为(0.5,0.5),那么锚点实际上所表示的就是(240,160)点的位置,那么我们在设置节点的位置坐标时,就会和这个锚点相关,如果我们将其中mRelativeAnchorPoint值设置为true,那么我们所设置的节点的位置就是这么锚点的相对位置,可能比较南里理解,我们在后面具体的使用过程中将进一步给大家分析。大家可以看都我们创建了一个子节点列表和一个父节点,因此说明我们的节点也是可以嵌套的。另一个需要分析的就是节点的摄像机Camera了,稍后将给大家详细分析,现在我们先来分析节点类的具体实现过程,由于篇幅关系对于该类的set和get函数就不进行解析了。
节点列表操作
首先我们来分析整个节点链,即子节点的一些常用操作,如代码清单2-3所示。
代码清单2-3:节点列表操作
view plaincopy
to clipboardprint?
//添加节点
public Node addChild(Node child) {
assert (child != null);
return addChild(child, child.mZOrder, child.mTag);
}
//添加节点
public Node addChild(Node child, int z) {
assert (child != null);
return addChild(child, z, child.mTag);
}
//(节点,z值,tag标记)
public Node addChild(Node child, int z, int tag) {
assert (child != null);
assert (child.mParent == null);
if (this.mChildren == null) {
childrenAlloc();
}
insertChild(child, z);
child.mTag = tag;
child.setParent(this);
if (this.mRunning) {
child.onEnter();
}
return this;
}
//新建列表
private void childrenAlloc() {
this.mChildren = new ArrayList(4);
}
//清理
public void cleanup() {
if (this.mChildren != null)
for (int i = 0; i < this.mChildren.size(); ++i)
((Node) this.mChildren.get(i)).cleanup();
}
//裁剪节点(移除节点)
private void detachChild(Node child, boolean doCleanup) {
if (doCleanup) {//移除前判断是否需要清理
child.cleanup();
}
child.setParent(null);
this.mChildren.remove(child);
}
//得到节点
public Node getChild(int tag) {
assert (tag != INVALID_TAG) : "Invalid tag";
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.mTag == tag) {
return child;
}
}
}
return null;
}
public ArrayList<Node> getChildren() {
return this.mChildren;
}
//插入节点
private void insertChild(Node node, int z) {
boolean added = false;
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.getZOrder() > z) {
added = true;
this.mChildren.add(i, node);
break;
}
}
if (!added)
this.mChildren.add(node);
node.setZOrder(z);
}
//移除所有节点
public void removeAllChildren(boolean cleanup) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (cleanup) {
child.cleanup();
}
child.setParent(null);
}
this.mChildren.clear();
}
//移除节点
public void removeChild(Node child, boolean cleanup) {
if (child == null) {
return;
}
if (this.mChildren.contains(child))
detachChild(child, cleanup);
}
public void removeChild(int tag, boolean cleanup) {
assert (tag != INVALID_TAG);
Node child = getChild(tag);
if (child == null)
Log.w("Engine", "removeChild: child not found");
else
removeChild(child, cleanup);
}
//重新排序节点
public void reorderChild(Node child, int zOrder) {
assert (child != null) : "Child must be non-null";
this.mChildren.remove(child);
insertChild(child, zOrder);
}
坐标转换系统
在上一篇文章我们说过,Android中坐标系和Opengl坐标系不一样,因此我们在使用Opengl ES做游戏时,时常需要对坐标系进行转换,说得更具体就是每一个元素在进行变换、事件处理时都需要将Android坐标系转换成Opengl坐标系,或者相反,因此我们在节点类中也对这些常用的处理进行了实现,对于坐标系的转换如代码清单2-4所示。
代码清单2-4:坐标系转换
view plaincopy
to clipboardprint?
//把一个全局坐标转换成节点内坐标
public YFSPoint convertToNodeSpace(float x, float y) {
YFSPoint worldPoint = YFSPoint.make(x, y);
return worldPoint.applyTransform(worldToNodeTransform());
}
//把一个全局坐标转换成节点内坐标,以节点的锚点为原点
public YFSPoint convertToNodeSpaceAR(float x, float y) {
YFSPoint nodePoint = convertToNodeSpace(x, y);
return YFSPoint.sub(nodePoint, this.mAnchorPosition);
}
//把Android触摸事件中坐标转换成节点内坐标。
public YFSPoint convertTouchToNodeSpace(MotionEvent event) {
YFSPoint point = Director.getInstance().convertToGL(event.getX(),
event.getY());
return convertToNodeSpace(point.x, point.y);
}
//把Android触摸事件中坐标转换成节点内坐标。
public YFSPoint convertTouchToNodeSpaceAR(MotionEvent event) {
YFSPoint point = Director.getInstance().convertToGL(event.getX(),
event.getY());
return convertToNodeSpaceAR(point.x, point.y);
}
//把节点内坐标转换为OpenGL全局坐标
public YFSPoint convertToWorldSpace(float x, float y) {
YFSPoint nodePoint = YFSPoint.make(x, y);
return nodePoint.applyTransform(nodeToWorldTransform());
}
//把节点内坐标转换为OpenGL全局坐标。
public YFSPoint convertToWorldSpaceAR(float x, float y) {
YFSPoint nodePoint = YFSPoint.make(x, y);
nodePoint = YFSPoint.add(nodePoint, this.mAnchorPosition);
return convertToWorldSpace(nodePoint.x, nodePoint.y);
}
大家可以看到其实就是使用了我们上一篇文章所给大家介绍的YFSPoint的applyTransform操作,而最终则会通过YFSAffineTransform来进行转换操作,有了大家对坐标系的理解,相信转换这个坐标系就很简单了,我们不在重复叙述了,这里还需要介绍一个比较重要的操作,就是将矩阵转换成矩形,具体转换实现如代码清单2-5所示。
代码清单2-5:convertRectUsingMatrix实现
view plaincopy
to clipboardprint?
private static YFSRect convertRectUsingMatrix(YFSRect aRect,
YFSAffineTransform matrix) {
YFSRect r = YFSRect.make(0.0F, 0.0F, 0.0F, 0.0F);
YFSPoint[] p = new YFSPoint[4];
for (int i = 0; i < 4; ++i) {
p[i] = YFSPoint.make(aRect.origin.x, aRect.origin.y);
}
p[1].x += aRect.size.width;
p[2].y += aRect.size.height;
p[3].x += aRect.size.width;
p[3].y += aRect.size.height;
for (int i = 0; i < 4; ++i) {
p[i] = p[i].applyTransform(matrix);
}
YFSPoint min = YFSPoint.make(p[0].x, p[0].y);
YFSPoint max = YFSPoint.make(p[0].x, p[0].y);
for (int i = 1; i < 4; ++i) {
min.x = Math.min(min.x, p[i].x);
min.y = Math.min(min.y, p[i].y);
max.x = Math.max(max.x, p[i].x);
max.y = Math.max(max.y, p[i].y);
}
r.origin.x = min.x;
r.origin.y = min.y;
r.size.width = (max.x - min.x);
r.size.height = (max.y - min.y);
return r;
}
由于我们的节点可以嵌套,所以节点的位置也就会受到其父节点的影响,因此我们提供了得到相对位置的函数,如代码清单2-6所示。
代码清单2-6:getAbsolutePosition实现
view plaincopy
to clipboardprint?
public YFSPoint getAbsolutePosition() {
YFSPoint ret = YFSPoint.make(this.mPosition.x, this.mPosition.y);
Node cn = this;
while (cn.mParent != null) {
cn = cn.mParent;
ret.x += cn.mPosition.x;
ret.y += cn.mPosition.y;
}
return ret;
}
其实现原理非常简单,我们判断其父节点是否为NULL,如果不围NULL,则在当前节点位置的基础上加上父节点的位置,就可以得到相对于父节点的位置。有了这些方法,我们在使用节点时就可以很方便了,这些操作需要一些数学方面的知识,大家在看的同时,可以参考一些数学资料,这样将有助于理解。
碰撞包围盒
当场景元素作为游戏开发的sprite时,我们很可能会对其进行碰撞检测,因此我们需要对节点设置其包围盒,以方便我们进行碰撞检测,如代码清单2-7所示,计算我们为每个节点设置的包围盒。
代码清单2-7:节点包围盒
//得到包围盒,相对于节点自身坐标系而言
view plaincopy
to clipboardprint?
public YFSRect getBoundingBox() {
return YFSRect.make(0.0F, 0.0F, this.mContentSize.width,
this.mContentSize.height);
}
// 得到节点的边框矩形,相对于父节点坐标系而言
public YFSRect getBoundingBoxRelativeToParent() {
YFSRect rect = YFSRect.make(0.0F, 0.0F, this.mContentSize.width,
this.mContentSize.height);
return convertRectUsingMatrix(rect, nodeToParentTransform());
}
//得到节点的边框矩形,相对于全局坐标系而言,得到的矩形对象中,origin是左下角坐标
public YFSRect getBoundingBoxReletiveToWorld() {
YFSRect rect = YFSRect.make(0.0F, 0.0F, this.mContentSize.width,
this.mContentSize.height);
return convertRectUsingMatrix(rect, nodeToWorldTransform());
}
这里我们主要是取得节点的包围盒,具体的碰撞检测实现另作讨论,当然最简单的就是直接使用一些2D的物理引擎,比如:box2d和chipmunk,同样你也可以自己实现碰撞算法,这里需要说明的是我们提供了3个不同环境的包围盒,他们包括自身包围盒、相对父节点的包围盒、相对于全局坐标系的包围盒。
渲染系统
对一些常用的操作进行了实现之后,我们就开始渲染这些节点对象了,渲染时我们同样需要对该节点及其所有子节点都进行渲染,由于该类是任何节点的基类,所以我们并不需要真正去实现渲染的(draw函数),这些渲染我们将在节点的具体子类中去实现,这部分内容我们会在后面的组件一节中进行介绍,这里我们主要是实现一个渲染的流程,我们先来看看具体代码实现,在进行分析,如代码清单2-8所示。
代码清单2-8:渲染流程
view plaincopy
to clipboardprint?
//强制渲染当前节点,包括子节点
public void visit(GL10 gl) {
//是否可见
if (!this.mVisible) {
return;
}
//把当前的矩阵拷贝到栈中.
gl.glPushMatrix();
//变换操作
transform(gl);
//渲染子节点
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.mZOrder >= 0)
break;
child.visit(gl);
}
}
draw(gl);
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.mZOrder >= 0) {
child.visit(gl);
}
}
}
//回复渲染之前的矩阵,用最后压入栈的矩阵恢复为当前矩阵
gl.glPopMatrix();
}
从代码清单2-8中可以看出,节点的渲染流程为,首先判断节点时候可见,然后通过glPushMatrix将当前矩阵压入到栈中,即保存当然矩阵,以便恢复,然后进行节点的变换操作,最后才渲染节点及其子节点,完成之后需要通过glPopMatrix来恢复之前压入栈顶的矩阵。
知道了渲染的流程,下面我们来分析如何处理节点的变换操作,其实节点的变换操作在Opengl中就主要指旋转、缩放、平移等,其具体实现入代码清单2-9所示。
代码清单2-9:变换操作
view plaincopy
to clipboardprint?
//变换操作
protected void transform(GL10 gl) {
//如果与锚点有关,则需要处理
if (this.mRelativeAnchorPoint) {
gl.glTranslatef(-this.mAnchorPosition.x, -this.mAnchorPosition.y,
this.mVertexZ);
}
//平移
gl.glTranslatef(this.mPosition.x + this.mAnchorPosition.x,
this.mPosition.y + this.mAnchorPosition.y, this.mVertexZ);
//旋转
if (this.mRotation != 0.0F) {
gl.glRotatef(-this.mRotation, 0.0F, 0.0F, 1.0F);
}
//缩放
if ((this.mScaleX != 1.0F) || (this.mScaleY != 1.0F)) {
gl.glScalef(this.mScaleX, this.mScaleY, 1.0F);
}
//如果与锚点有关...
gl.glTranslatef(-this.mAnchorPosition.x, -this.mAnchorPosition.y,
this.mVertexZ);
}
在Opengl中旋转操作使用glRotatef、平移操作使用glTranslatef、缩放操作则使用glRotatef,这都很简单,唯一需要说明一下的是,如果我们的节点的mRelativeAnchorPoint为true则表示与锚点有关,那么我们在变换钱就需要先按照描点的相对位置来变换,然后进行节点自身的变换,最后再次按照描点的相对位置来进行平移变换。这样才能对节点渲染的位置进行准确的控制。
为了方便我们在对节点进行激活时进行某些操作,我们实现了onEnter和onExit接口,他们分别是当节点要变成活动态时,该方法被调用和当节点所属场景退出时该方法被调用,或者当该节点被删除时被调用,具体实现入代码清单2-10所示。
代码清单2-10:onEnter和onExit实现
view plaincopy
to clipboardprint?
public void onEnter() {
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
child.onEnter();
}
}
this.mRunning = true;
}
public void onExit() {
this.mRunning = false;
if (this.mChildren != null)
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
child.onExit();
}
}
实现了这样的接口之后,我们就可以在节点被添加(addnode)时调用onEnter函数,在将节点被移除时调用onExit函数,因此前面所实现的节点操作函数中就可以加上这样两个函数,使其更加完善,添加方法很简单,就是判断当前节点是否运行(mRunning),如果运行则调用该节点的上面这两个函数,所以大家可以自己下去实现即可。
其他接口
为了节点的属性更加完善,我们还提供了一些附加的接口,用来设置节点的特殊属性,比如:动画、颜色、透明度、贴图等。当前我们并不需要实现这些接口,但是我们这里先给定义出来,在具体的组件,节点子类中可能就会需要,其接口定义如代码清单2-11所示。
代码清单2-11:其他接口实现
view plaincopy
to clipboardprint?
//动画帧的基类
public static class Frame {
public float duration;
public Frame(float duration) {
this.duration = duration;
}
}
//动画接口
public static abstract interface IAnimation {
//时间
public abstract float getDuration();
//德奥帧
public abstract List<? extends Node.Frame> getFrames();
//得到名称
public abstract String getName();
}
//实现此接口表示节点可以设置渲染模式
public static abstract interface IBlendable {
//设置混合
public abstract void setBlendFunc(YFSBlendFunc paramYFSBlendFunc);
//得到混合
public abstract YFSBlendFunc getBlendFunc();
}
//实现该接口表示节点可以支持设置贴图且设置渲染方式
public static abstract interface IBlendableTextureOwner extends
Node.IBlendable, Node.ITextureOwner {
}
//一个组合接口,表示支持设置颜色和透明度
public static abstract interface IColorable extends Node.ITransparent,
Node.IRGB {
}
//一个组合接口,表示支持设置颜色和文字
public static abstract interface IColorableLabel extends Node.IColorable, Node.ILabel {
}
//实现此接口表示支持对单帧进行操作, 并且支持添加动画
public static abstract interface IFrames {
//添加动画
public abstract void addAnimation(Node.IAnimation paramIAnimation);
//得到动画名称
public abstract Node.IAnimation getAnimationByName(String paramString);
public abstract Node.Frame getDisplayFrame();
//是否显示
public abstract boolean isFrameDisplayed(Node.Frame paramFrame);
//设置显示帧
public abstract void setDisplayFrame(Node.Frame paramFrame);
public abstract void setDisplayFrame(String paramString, int paramInt);
}
//实现此接口表示节点支持设置文本
public static abstract interface ILabel {
public abstract void setText(String paramString);
}
//实现此接口表示节点支持设置颜色
public static abstract interface IRGB {
public abstract YFSColor3B getColor();
public abstract void setColor(YFSColor3B paramYFSColor3B);
}
//实现此接口表示可以得到大小信息
public static abstract interface ISizable {
public abstract float getHeight();
public abstract float getWidth();
}
//实现此接口表示节点可以包含一个贴图
public static abstract interface ITextureOwner {
public abstract Texture2D getTexture();
public abstract void setTexture(Texture2D paramTexture2D);
}
//实现此接口表示节点支持设置透明度
public static abstract interface ITransparent {
public abstract int getAlpha();
public abstract void setAlpha(int paramInt);
}
大家可以先对这些接口有一定的了解,后面具体实现时将对其进行详细介绍,另外该类还需要包含一些set和get属性的函数,这就非常简单了,大家可以查看我们提供的源码即可。最后该类还需要包含一个摄像头和一个计时器,这两部分内容比较重要我们将在本文末单独来进行介绍,如果你需要了解,也可以跳到后面去查看。
游戏场景
上面我们介绍了场景的节点,现在我们来学习场景,我们只所以把场景放在后面来介绍,就是因为该引擎中场景就是一个继承自节点(Node)类的一个子类,为什么会这样呢?这将和我们所定义的场景的定义相关。那么场景究竟是什么呢?
当我们在做一个游戏时,一般都有很多个界面,其实我们这里是把这每一个界面都看成是一个场景,界面中的任何元素也就是我们的场景元素,也就是节点(Node),由于我们前面知道了,节点事可以嵌套的,可以包含很多个子节点,所有这正好符合我们场景原则,场景中可以包含很多和节点元素。这样来看,大家应该能够理解了吧。因此在有了节点的基础上实现场景就很简单了,首先我们来看具体的代码,如代码清单2-12所示。
代码清单2-12:场景scene实现
view plaincopy
to clipboardprint?
public class Scene extends Node {
//取得scene实例
public static Scene make() {
return new Scene();
}
protected Scene() {
//得到窗口尺寸
YFSSize s = Director.getInstance().getWindowSize();
//设置不受锚点影响
setRelativeAnchorPoint(false);
//设置锚点
setAnchorPercent(0.5F, 0.5F);
//设置尺寸
setContentSize(s.width, s.height);
}
}
整个引擎的场景scene类就这么简单,他继承自Node类,因此具有Node类的通用方法,场景与一般节点位移不同的就是,我们在构造函数中设置锚点时,将锚点设置为(0.5,0.5)了,一般节点默认的锚点为(0,0),所以这也正是场景和一般节点不同,并且需要注意的地方,最后将场景的尺寸设置为窗口的尺寸即可。这样我们可以在场景中添加其他任何节点,作为该场景节点的子节点。
待续(二)
(声明:本网的新闻及文章版权均属OPhone SDN网站所有,如需转载请与我们编辑团队联系。任何媒体、网站或个人未经本网书面协议授权,不得进行任何形式的转载。已经取得本网协议授权的媒体、网站,在转载使用时请注明稿件来源。)
首先在YFSGLSurfaceView中声明以下成员:
view plaincopy
to clipboardprint?
//渲染器
private Director mDirector;
//事件调度器
private EventDispatcher mDispatcher;
然后,需要在构造函数中实例化这些对象,代码如下:
//取得Director
this.mDirector = Director.getInstance();
//取得context
this.mDirector.context = context;
//取得EventDispatcher
this.mDispatcher = EventDispatcher.getInstance();
同时在退出函数surfaceDestroyed中,我们将调用Director的onSurfaceDestroyed函数来处理退出操作,释放资源等,代码如下:
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
this.mDirector.onSurfaceDestroyed();
}
最后就是事件处理部分,由于我们对事件的处理都是在EventDispatcher中进行统一管理调度的,所以我们只需要按照不同的事件类型,将事件传递给EventDispatcher即可,EventDispatcher便会为我们进行事件处理,如代码清单2-1所示。
代码清单2-1:事件处理
view plaincopy
to clipboardprint?
//触笔事件处理
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
this.mDispatcher.touchesCancelled(event);
break;
case MotionEvent.ACTION_DOWN:
this.mDispatcher.touchesBegan(event);
break;
case MotionEvent.ACTION_MOVE:
this.mDispatcher.touchesMoved(event);
break;
case MotionEvent.ACTION_UP:
this.mDispatcher.touchesEnded(event);
}
return true;
}
//按键事件处理
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (this.mDispatcher.keyDown(event)) {
return true;
}
return super.onKeyDown(keyCode, event);
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (this.mDispatcher.keyUp(event)) {
return true;
}
return super.onKeyUp(keyCode, event);
}
好了,上面则是我们对上一篇引擎的框架进行完善,当然还有Director需要完善,我们会在后面组件进行完善。这里我们先来分析最基础的场景元素,即前面说的节点。
场景元素
从名字大家可以看出来,所谓的场景元素,那么肯定就是能够放置在场景中的任何对象,比如一棵树、一个怪物、一个按钮,一个标签等都是场景中的元素,甚至场景、图层本身也是一个场景元素,即节点。我们将在后面给大家分析,大家也可以跳到后面去看一下,这样更有助于理解场景元素的定义。
节点基类实现
我们可以为这所有的元素抽象一个基类Node,任何可以在场景中放置的元素都将继承自该类。Node封装了各种元素的的通用逻辑和渲染过程,这里我们也把这些元素称之为“节点”,在后文中出现的节点就表示元素。首先我们要确定一个通用节点需要哪些属性?如代码清单2-2所示。
代码清单2-2:节点的部分属性
view plaincopy
to clipboardprint?
public class Node {
protected static final int INVALID_TAG = -1;
//锚点比例
protected PointF mAnchorPercent;
//摄像头
private Camera mCamera;
//子节点
protected ArrayList<Node> mChildren;
//尺寸大小
protected YFSSize mContentSize;
private BaseGrid mGrid;
//是否运行中...
private boolean mRunning;
//变换
protected boolean mTransformDirty;
protected boolean mInverseDirty;
//父节点
private Node mParent;
//位置
protected YFSPoint mPosition;
//锚点是否发生作用
private boolean mRelativeAnchorPoint;
//是否显示
private boolean mEnabled;
//是否选中
private boolean mSelected;
//旋转,缩放
private float mRotation;
private float mScaleX;
private float mScaleY;
//tag标记
protected int mTag;
//放射变换
private YFSAffineTransform mTransformMatrix;
private YFSAffineTransform mInverseMatrix;
//锚点位置
protected YFSPoint mAnchorPosition;
//其他附加数据
private Object mUserData;
//节点在OpenGL中的z order值
private float mVertexZ;
//是否可见
protected boolean mVisible;
//节点在父节点中的z order值
private int mZOrder;
//...
}
代码清单2-2列出了一个通用节点所需要的常用属性,另外每个节点还可以设置其动画,我们将在引擎的动画部分完成之后在来完善节点的动画,大家可以根据注解来理解,这里我们主要说明一下“锚点”,我们对每一个节点都设置了一个锚点,比如一个节点的大小尺寸为480*320,锚点被设置为(0.5,0.5),那么锚点实际上所表示的就是(240,160)点的位置,那么我们在设置节点的位置坐标时,就会和这个锚点相关,如果我们将其中mRelativeAnchorPoint值设置为true,那么我们所设置的节点的位置就是这么锚点的相对位置,可能比较南里理解,我们在后面具体的使用过程中将进一步给大家分析。大家可以看都我们创建了一个子节点列表和一个父节点,因此说明我们的节点也是可以嵌套的。另一个需要分析的就是节点的摄像机Camera了,稍后将给大家详细分析,现在我们先来分析节点类的具体实现过程,由于篇幅关系对于该类的set和get函数就不进行解析了。
节点列表操作
首先我们来分析整个节点链,即子节点的一些常用操作,如代码清单2-3所示。
代码清单2-3:节点列表操作
view plaincopy
to clipboardprint?
//添加节点
public Node addChild(Node child) {
assert (child != null);
return addChild(child, child.mZOrder, child.mTag);
}
//添加节点
public Node addChild(Node child, int z) {
assert (child != null);
return addChild(child, z, child.mTag);
}
//(节点,z值,tag标记)
public Node addChild(Node child, int z, int tag) {
assert (child != null);
assert (child.mParent == null);
if (this.mChildren == null) {
childrenAlloc();
}
insertChild(child, z);
child.mTag = tag;
child.setParent(this);
if (this.mRunning) {
child.onEnter();
}
return this;
}
//新建列表
private void childrenAlloc() {
this.mChildren = new ArrayList(4);
}
//清理
public void cleanup() {
if (this.mChildren != null)
for (int i = 0; i < this.mChildren.size(); ++i)
((Node) this.mChildren.get(i)).cleanup();
}
//裁剪节点(移除节点)
private void detachChild(Node child, boolean doCleanup) {
if (doCleanup) {//移除前判断是否需要清理
child.cleanup();
}
child.setParent(null);
this.mChildren.remove(child);
}
//得到节点
public Node getChild(int tag) {
assert (tag != INVALID_TAG) : "Invalid tag";
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.mTag == tag) {
return child;
}
}
}
return null;
}
public ArrayList<Node> getChildren() {
return this.mChildren;
}
//插入节点
private void insertChild(Node node, int z) {
boolean added = false;
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.getZOrder() > z) {
added = true;
this.mChildren.add(i, node);
break;
}
}
if (!added)
this.mChildren.add(node);
node.setZOrder(z);
}
//移除所有节点
public void removeAllChildren(boolean cleanup) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (cleanup) {
child.cleanup();
}
child.setParent(null);
}
this.mChildren.clear();
}
//移除节点
public void removeChild(Node child, boolean cleanup) {
if (child == null) {
return;
}
if (this.mChildren.contains(child))
detachChild(child, cleanup);
}
public void removeChild(int tag, boolean cleanup) {
assert (tag != INVALID_TAG);
Node child = getChild(tag);
if (child == null)
Log.w("Engine", "removeChild: child not found");
else
removeChild(child, cleanup);
}
//重新排序节点
public void reorderChild(Node child, int zOrder) {
assert (child != null) : "Child must be non-null";
this.mChildren.remove(child);
insertChild(child, zOrder);
}
坐标转换系统
在上一篇文章我们说过,Android中坐标系和Opengl坐标系不一样,因此我们在使用Opengl ES做游戏时,时常需要对坐标系进行转换,说得更具体就是每一个元素在进行变换、事件处理时都需要将Android坐标系转换成Opengl坐标系,或者相反,因此我们在节点类中也对这些常用的处理进行了实现,对于坐标系的转换如代码清单2-4所示。
代码清单2-4:坐标系转换
view plaincopy
to clipboardprint?
//把一个全局坐标转换成节点内坐标
public YFSPoint convertToNodeSpace(float x, float y) {
YFSPoint worldPoint = YFSPoint.make(x, y);
return worldPoint.applyTransform(worldToNodeTransform());
}
//把一个全局坐标转换成节点内坐标,以节点的锚点为原点
public YFSPoint convertToNodeSpaceAR(float x, float y) {
YFSPoint nodePoint = convertToNodeSpace(x, y);
return YFSPoint.sub(nodePoint, this.mAnchorPosition);
}
//把Android触摸事件中坐标转换成节点内坐标。
public YFSPoint convertTouchToNodeSpace(MotionEvent event) {
YFSPoint point = Director.getInstance().convertToGL(event.getX(),
event.getY());
return convertToNodeSpace(point.x, point.y);
}
//把Android触摸事件中坐标转换成节点内坐标。
public YFSPoint convertTouchToNodeSpaceAR(MotionEvent event) {
YFSPoint point = Director.getInstance().convertToGL(event.getX(),
event.getY());
return convertToNodeSpaceAR(point.x, point.y);
}
//把节点内坐标转换为OpenGL全局坐标
public YFSPoint convertToWorldSpace(float x, float y) {
YFSPoint nodePoint = YFSPoint.make(x, y);
return nodePoint.applyTransform(nodeToWorldTransform());
}
//把节点内坐标转换为OpenGL全局坐标。
public YFSPoint convertToWorldSpaceAR(float x, float y) {
YFSPoint nodePoint = YFSPoint.make(x, y);
nodePoint = YFSPoint.add(nodePoint, this.mAnchorPosition);
return convertToWorldSpace(nodePoint.x, nodePoint.y);
}
大家可以看到其实就是使用了我们上一篇文章所给大家介绍的YFSPoint的applyTransform操作,而最终则会通过YFSAffineTransform来进行转换操作,有了大家对坐标系的理解,相信转换这个坐标系就很简单了,我们不在重复叙述了,这里还需要介绍一个比较重要的操作,就是将矩阵转换成矩形,具体转换实现如代码清单2-5所示。
代码清单2-5:convertRectUsingMatrix实现
view plaincopy
to clipboardprint?
private static YFSRect convertRectUsingMatrix(YFSRect aRect,
YFSAffineTransform matrix) {
YFSRect r = YFSRect.make(0.0F, 0.0F, 0.0F, 0.0F);
YFSPoint[] p = new YFSPoint[4];
for (int i = 0; i < 4; ++i) {
p[i] = YFSPoint.make(aRect.origin.x, aRect.origin.y);
}
p[1].x += aRect.size.width;
p[2].y += aRect.size.height;
p[3].x += aRect.size.width;
p[3].y += aRect.size.height;
for (int i = 0; i < 4; ++i) {
p[i] = p[i].applyTransform(matrix);
}
YFSPoint min = YFSPoint.make(p[0].x, p[0].y);
YFSPoint max = YFSPoint.make(p[0].x, p[0].y);
for (int i = 1; i < 4; ++i) {
min.x = Math.min(min.x, p[i].x);
min.y = Math.min(min.y, p[i].y);
max.x = Math.max(max.x, p[i].x);
max.y = Math.max(max.y, p[i].y);
}
r.origin.x = min.x;
r.origin.y = min.y;
r.size.width = (max.x - min.x);
r.size.height = (max.y - min.y);
return r;
}
由于我们的节点可以嵌套,所以节点的位置也就会受到其父节点的影响,因此我们提供了得到相对位置的函数,如代码清单2-6所示。
代码清单2-6:getAbsolutePosition实现
view plaincopy
to clipboardprint?
public YFSPoint getAbsolutePosition() {
YFSPoint ret = YFSPoint.make(this.mPosition.x, this.mPosition.y);
Node cn = this;
while (cn.mParent != null) {
cn = cn.mParent;
ret.x += cn.mPosition.x;
ret.y += cn.mPosition.y;
}
return ret;
}
其实现原理非常简单,我们判断其父节点是否为NULL,如果不围NULL,则在当前节点位置的基础上加上父节点的位置,就可以得到相对于父节点的位置。有了这些方法,我们在使用节点时就可以很方便了,这些操作需要一些数学方面的知识,大家在看的同时,可以参考一些数学资料,这样将有助于理解。
碰撞包围盒
当场景元素作为游戏开发的sprite时,我们很可能会对其进行碰撞检测,因此我们需要对节点设置其包围盒,以方便我们进行碰撞检测,如代码清单2-7所示,计算我们为每个节点设置的包围盒。
代码清单2-7:节点包围盒
//得到包围盒,相对于节点自身坐标系而言
view plaincopy
to clipboardprint?
public YFSRect getBoundingBox() {
return YFSRect.make(0.0F, 0.0F, this.mContentSize.width,
this.mContentSize.height);
}
// 得到节点的边框矩形,相对于父节点坐标系而言
public YFSRect getBoundingBoxRelativeToParent() {
YFSRect rect = YFSRect.make(0.0F, 0.0F, this.mContentSize.width,
this.mContentSize.height);
return convertRectUsingMatrix(rect, nodeToParentTransform());
}
//得到节点的边框矩形,相对于全局坐标系而言,得到的矩形对象中,origin是左下角坐标
public YFSRect getBoundingBoxReletiveToWorld() {
YFSRect rect = YFSRect.make(0.0F, 0.0F, this.mContentSize.width,
this.mContentSize.height);
return convertRectUsingMatrix(rect, nodeToWorldTransform());
}
这里我们主要是取得节点的包围盒,具体的碰撞检测实现另作讨论,当然最简单的就是直接使用一些2D的物理引擎,比如:box2d和chipmunk,同样你也可以自己实现碰撞算法,这里需要说明的是我们提供了3个不同环境的包围盒,他们包括自身包围盒、相对父节点的包围盒、相对于全局坐标系的包围盒。
渲染系统
对一些常用的操作进行了实现之后,我们就开始渲染这些节点对象了,渲染时我们同样需要对该节点及其所有子节点都进行渲染,由于该类是任何节点的基类,所以我们并不需要真正去实现渲染的(draw函数),这些渲染我们将在节点的具体子类中去实现,这部分内容我们会在后面的组件一节中进行介绍,这里我们主要是实现一个渲染的流程,我们先来看看具体代码实现,在进行分析,如代码清单2-8所示。
代码清单2-8:渲染流程
view plaincopy
to clipboardprint?
//强制渲染当前节点,包括子节点
public void visit(GL10 gl) {
//是否可见
if (!this.mVisible) {
return;
}
//把当前的矩阵拷贝到栈中.
gl.glPushMatrix();
//变换操作
transform(gl);
//渲染子节点
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.mZOrder >= 0)
break;
child.visit(gl);
}
}
draw(gl);
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
if (child.mZOrder >= 0) {
child.visit(gl);
}
}
}
//回复渲染之前的矩阵,用最后压入栈的矩阵恢复为当前矩阵
gl.glPopMatrix();
}
从代码清单2-8中可以看出,节点的渲染流程为,首先判断节点时候可见,然后通过glPushMatrix将当前矩阵压入到栈中,即保存当然矩阵,以便恢复,然后进行节点的变换操作,最后才渲染节点及其子节点,完成之后需要通过glPopMatrix来恢复之前压入栈顶的矩阵。
知道了渲染的流程,下面我们来分析如何处理节点的变换操作,其实节点的变换操作在Opengl中就主要指旋转、缩放、平移等,其具体实现入代码清单2-9所示。
代码清单2-9:变换操作
view plaincopy
to clipboardprint?
//变换操作
protected void transform(GL10 gl) {
//如果与锚点有关,则需要处理
if (this.mRelativeAnchorPoint) {
gl.glTranslatef(-this.mAnchorPosition.x, -this.mAnchorPosition.y,
this.mVertexZ);
}
//平移
gl.glTranslatef(this.mPosition.x + this.mAnchorPosition.x,
this.mPosition.y + this.mAnchorPosition.y, this.mVertexZ);
//旋转
if (this.mRotation != 0.0F) {
gl.glRotatef(-this.mRotation, 0.0F, 0.0F, 1.0F);
}
//缩放
if ((this.mScaleX != 1.0F) || (this.mScaleY != 1.0F)) {
gl.glScalef(this.mScaleX, this.mScaleY, 1.0F);
}
//如果与锚点有关...
gl.glTranslatef(-this.mAnchorPosition.x, -this.mAnchorPosition.y,
this.mVertexZ);
}
在Opengl中旋转操作使用glRotatef、平移操作使用glTranslatef、缩放操作则使用glRotatef,这都很简单,唯一需要说明一下的是,如果我们的节点的mRelativeAnchorPoint为true则表示与锚点有关,那么我们在变换钱就需要先按照描点的相对位置来变换,然后进行节点自身的变换,最后再次按照描点的相对位置来进行平移变换。这样才能对节点渲染的位置进行准确的控制。
为了方便我们在对节点进行激活时进行某些操作,我们实现了onEnter和onExit接口,他们分别是当节点要变成活动态时,该方法被调用和当节点所属场景退出时该方法被调用,或者当该节点被删除时被调用,具体实现入代码清单2-10所示。
代码清单2-10:onEnter和onExit实现
view plaincopy
to clipboardprint?
public void onEnter() {
if (this.mChildren != null) {
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
child.onEnter();
}
}
this.mRunning = true;
}
public void onExit() {
this.mRunning = false;
if (this.mChildren != null)
for (int i = 0; i < this.mChildren.size(); ++i) {
Node child = (Node) this.mChildren.get(i);
child.onExit();
}
}
实现了这样的接口之后,我们就可以在节点被添加(addnode)时调用onEnter函数,在将节点被移除时调用onExit函数,因此前面所实现的节点操作函数中就可以加上这样两个函数,使其更加完善,添加方法很简单,就是判断当前节点是否运行(mRunning),如果运行则调用该节点的上面这两个函数,所以大家可以自己下去实现即可。
其他接口
为了节点的属性更加完善,我们还提供了一些附加的接口,用来设置节点的特殊属性,比如:动画、颜色、透明度、贴图等。当前我们并不需要实现这些接口,但是我们这里先给定义出来,在具体的组件,节点子类中可能就会需要,其接口定义如代码清单2-11所示。
代码清单2-11:其他接口实现
view plaincopy
to clipboardprint?
//动画帧的基类
public static class Frame {
public float duration;
public Frame(float duration) {
this.duration = duration;
}
}
//动画接口
public static abstract interface IAnimation {
//时间
public abstract float getDuration();
//德奥帧
public abstract List<? extends Node.Frame> getFrames();
//得到名称
public abstract String getName();
}
//实现此接口表示节点可以设置渲染模式
public static abstract interface IBlendable {
//设置混合
public abstract void setBlendFunc(YFSBlendFunc paramYFSBlendFunc);
//得到混合
public abstract YFSBlendFunc getBlendFunc();
}
//实现该接口表示节点可以支持设置贴图且设置渲染方式
public static abstract interface IBlendableTextureOwner extends
Node.IBlendable, Node.ITextureOwner {
}
//一个组合接口,表示支持设置颜色和透明度
public static abstract interface IColorable extends Node.ITransparent,
Node.IRGB {
}
//一个组合接口,表示支持设置颜色和文字
public static abstract interface IColorableLabel extends Node.IColorable, Node.ILabel {
}
//实现此接口表示支持对单帧进行操作, 并且支持添加动画
public static abstract interface IFrames {
//添加动画
public abstract void addAnimation(Node.IAnimation paramIAnimation);
//得到动画名称
public abstract Node.IAnimation getAnimationByName(String paramString);
public abstract Node.Frame getDisplayFrame();
//是否显示
public abstract boolean isFrameDisplayed(Node.Frame paramFrame);
//设置显示帧
public abstract void setDisplayFrame(Node.Frame paramFrame);
public abstract void setDisplayFrame(String paramString, int paramInt);
}
//实现此接口表示节点支持设置文本
public static abstract interface ILabel {
public abstract void setText(String paramString);
}
//实现此接口表示节点支持设置颜色
public static abstract interface IRGB {
public abstract YFSColor3B getColor();
public abstract void setColor(YFSColor3B paramYFSColor3B);
}
//实现此接口表示可以得到大小信息
public static abstract interface ISizable {
public abstract float getHeight();
public abstract float getWidth();
}
//实现此接口表示节点可以包含一个贴图
public static abstract interface ITextureOwner {
public abstract Texture2D getTexture();
public abstract void setTexture(Texture2D paramTexture2D);
}
//实现此接口表示节点支持设置透明度
public static abstract interface ITransparent {
public abstract int getAlpha();
public abstract void setAlpha(int paramInt);
}
大家可以先对这些接口有一定的了解,后面具体实现时将对其进行详细介绍,另外该类还需要包含一些set和get属性的函数,这就非常简单了,大家可以查看我们提供的源码即可。最后该类还需要包含一个摄像头和一个计时器,这两部分内容比较重要我们将在本文末单独来进行介绍,如果你需要了解,也可以跳到后面去查看。
游戏场景
上面我们介绍了场景的节点,现在我们来学习场景,我们只所以把场景放在后面来介绍,就是因为该引擎中场景就是一个继承自节点(Node)类的一个子类,为什么会这样呢?这将和我们所定义的场景的定义相关。那么场景究竟是什么呢?
当我们在做一个游戏时,一般都有很多个界面,其实我们这里是把这每一个界面都看成是一个场景,界面中的任何元素也就是我们的场景元素,也就是节点(Node),由于我们前面知道了,节点事可以嵌套的,可以包含很多个子节点,所有这正好符合我们场景原则,场景中可以包含很多和节点元素。这样来看,大家应该能够理解了吧。因此在有了节点的基础上实现场景就很简单了,首先我们来看具体的代码,如代码清单2-12所示。
代码清单2-12:场景scene实现
view plaincopy
to clipboardprint?
public class Scene extends Node {
//取得scene实例
public static Scene make() {
return new Scene();
}
protected Scene() {
//得到窗口尺寸
YFSSize s = Director.getInstance().getWindowSize();
//设置不受锚点影响
setRelativeAnchorPoint(false);
//设置锚点
setAnchorPercent(0.5F, 0.5F);
//设置尺寸
setContentSize(s.width, s.height);
}
}
整个引擎的场景scene类就这么简单,他继承自Node类,因此具有Node类的通用方法,场景与一般节点位移不同的就是,我们在构造函数中设置锚点时,将锚点设置为(0.5,0.5)了,一般节点默认的锚点为(0,0),所以这也正是场景和一般节点不同,并且需要注意的地方,最后将场景的尺寸设置为窗口的尺寸即可。这样我们可以在场景中添加其他任何节点,作为该场景节点的子节点。
待续(二)
(声明:本网的新闻及文章版权均属OPhone SDN网站所有,如需转载请与我们编辑团队联系。任何媒体、网站或个人未经本网书面协议授权,不得进行任何形式的转载。已经取得本网协议授权的媒体、网站,在转载使用时请注明稿件来源。)
相关文章推荐
- OPhone平台2D游戏引擎实现——概述及框架(一)
- Android/Ophone平台2D游戏引擎实现系列文章
- Android/Ophone平台2D游戏引擎实现系列文章 推荐
- Ophone平台2D游戏引擎实现——物理引擎(一)(二)个人整理
- 一种2D游戏引擎的设计与实现
- 移动平台的2d游戏引擎(android&iphone)
- Nokia发布Symbian平台上C++语言的2D游戏引擎(含源代码)
- 一种2D游戏引擎的设计与实现
- 一种2D游戏引擎的设计与实现
- 一种2D游戏引擎的设计与实现
- 一种2D游戏引擎的设计与实现
- 非典型2D游戏引擎 Orx 源码阅读笔记(4) 用C实现的基本容器(List,HashTable,Tree)
- Cocos2d-x 是一个支持多平台的 2D 手机游戏引擎
- 动手实现2d游戏引擎(一)
- 非典型2D游戏引擎 Orx 源码阅读笔记(6) C语言实现的面向对象
- 一个容易上手的2D游戏引擎love2d (lua)
- arkit-tictactoe:ARKit 外加简单交互,实现 AR 场景的三连棋游戏演
- Symbian多平台游戏引擎开发日记 2006-4-24
- 国外 2D 游戏引擎相关站点
- 2D平台游戏王牌英雄的AI寻路解决方案