上一篇我们对游戏引擎的框架进行了分析,同时也完成了一个基础的游戏引擎框架,那么这一篇我们将完成游戏引擎中的场景,图层,元素(节点)进行实现,但是在开始之前,我们可以对上一篇所介绍的框架进行完善,首先我们可以对引擎的渲染窗口(YFSGLSurfaceView)进行完善,使其具备事件处理能力和对渲染器(Renderer)进行控制。
首先在YFSGLSurfaceView中声明以下成员:
view plain copy to clipboard print ?
-
- private Director mDirector;
-
- private EventDispatcher mDispatcher;
- 然后,需要在构造函数中实例化这些对象,代码如下:
-
- this.mDirector = Director.getInstance();
-
- this.mDirector.context = context;
-
- 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 plain copy to clipboard print ?
-
- 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 plain copy to clipboard print ?
- 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;
-
- protected int mTag;
-
- private YFSAffineTransform mTransformMatrix;
- private YFSAffineTransform mInverseMatrix;
-
- protected YFSPoint mAnchorPosition;
-
- private Object mUserData;
-
- private float mVertexZ;
-
- protected boolean mVisible;
-
- private int mZOrder;
-
- }
代码清单2-2列出了一个通用节点所需要的常用属性,另外每个节点还可以设置其动画,我们将在引擎的动画部分完成之后在来完善节点的动画,大家可以根据注解来理解,这里我们主要说明一下“锚点”,我们对每一个节点都设置了一个锚点,比如一个节点的大小尺寸为480*320,锚点被设置为(0.5,0.5),那么锚点实际上所表示的就是(240,160)点的位置,那么我们在设置节点的位置坐标时,就会和这个锚点相关,如果我们将其中mRelativeAnchorPoint值设置为true,那么我们所设置的节点的位置就是这么锚点的相对位置,可能比较南里理解,我们在后面具体的使用过程中将进一步给大家分析。大家可以看都我们创建了一个子节点列表和一个父节点,因此说明我们的节点也是可以嵌套的。另一个需要分析的就是节点的摄像机Camera了,稍后将给大家详细分析,现在我们先来分析节点类的具体实现过程,由于篇幅关系对于该类的set和get函数就不进行解析了。
节点列表操作
首先我们来分析整个节点链,即子节点的一些常用操作,如代码清单2-3所示。
代码清单2-3:节点列表操作
view plain copy to clipboard print ?
-
- 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);
- }
-
- 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 plain copy to clipboard print ?
-
- 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);
- }
-
- public YFSPoint convertTouchToNodeSpace(MotionEvent event) {
- YFSPoint point = Director.getInstance().convertToGL(event.getX(),
- event.getY());
- return convertToNodeSpace(point.x, point.y);
- }
-
- public YFSPoint convertTouchToNodeSpaceAR(MotionEvent event) {
- YFSPoint point = Director.getInstance().convertToGL(event.getX(),
- event.getY());
- return convertToNodeSpaceAR(point.x, point.y);
- }
-
- public YFSPoint convertToWorldSpace(float x, float y) {
- YFSPoint nodePoint = YFSPoint.make(x, y);
- return nodePoint.applyTransform(nodeToWorldTransform());
- }
-
- 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 plain copy to clipboard print ?
- 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 plain copy to clipboard print ?
- 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 plain copy to clipboard print ?
- 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());
- }
-
- 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 plain copy to clipboard print ?
-
- 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 plain copy to clipboard print ?
-
- 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 plain copy to clipboard print ?
- 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 plain copy to clipboard print ?
-
- 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 plain copy to clipboard print ?
- public class Scene extends Node {
-
- 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网站所有,如需转载请与我们编辑团队联系。任何媒体、网站或个人未经本网书面协议授权,不得进行任何形式的转载。已经取得本网协议授权的媒体、网站,在转载使用时请注明稿件来源。)