您的位置:首页 > 运维架构

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网站所有,如需转载请与我们编辑团队联系。任何媒体、网站或个人未经本网书面协议授权,不得进行任何形式的转载。已经取得本网协议授权的媒体、网站,在转载使用时请注明稿件来源。)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: