libgdx学习笔记系列(五)游戏的MVC结构
程序员文章站
2022-05-21 09:45:49
...
先看看游戏的mvc结构如何实现。先看下包结构分类
在开始之前需要做些准备工作。人物图片一共有16张。如果循环加载无疑会加重系统负担。
这还只是一个人物,试想如果一个游戏中有好多的人物,那肯定会影响游戏运行。并且资源的加载方式也需要改进。
web的前端开发中会把使用到的图片合并。这样利于加载。libgdx也可以实现这种效果。我们先利用libgdx提供的工具合并下图片。让它变成一张图片。
进入libgdx源码,定位到我们编译出的dist文件夹。(从网上下载的官方压缩好的压缩包的,直接解压,进入根目录就可以了。)新建一个in文件夹,新建一个out文件夹 拷贝16张图片到in文件夹。然后使用命令行进入到dist文件夹根目录(下载压缩包的,直接进入解压后目录)
输入如下命令
好了,拷贝生成的两个文件到我们的项目assets目录,删除掉原来的16张。
现在思考下这个游戏,其实不算游戏,只是一个小人在跑动而已。
如何让游戏更加真实呢。比如人跑动的速度,与屏幕大小的关系。如果更准确的表述出来呢。
这就需要我们对整个游戏世界有个把握。要知道手机屏幕的大小是不确定的。同样我们制做的纹理如果要考虑到大小问题。想想一个大型游戏,上百张图片,我们都要控制好它的大小。与实际游戏屏幕进行匹配,是不是很头疼,我们这个游戏中人物太小了。如果要调整大小,我只能调整纹理,或者调整渲染时的大小。当然有更好的办法。我们用比例来理解屏幕。
下面我在actor包中新建一个world类,来更好的控制我们的世界
为了方便理解上面的代码我专门做了一张图片
我们看上图,假设 屏幕分辨率为800*480 。我们分成10*6的方格。每个方格大小就是80*80像素大小的正方形。也就是我们使用setPixelsXY(int width, int height) 方法计算出的pixelsX,pixelsY的大小。
那么我们以后定义演员大小,速度等的时候就可以很直观的使用比例来定义。
例如我们定义演员为2f,也就是两个单位的方格。在渲染人物的时候我们直接使用演员的大小
size*pixels 这就是实际演员的像素大小。这样就很方便的定义游戏中的物体大小。和进行边界检查,碰撞等操作。我们没必要关心纹理大小和屏幕大小,只需要知道我们的游戏世界中的大小为10*6的,根据这个大小来调整演员等等物体的大小就可以了。很方便不是。
前面我们的演员代码并没有体现出这个演员的大小位置等等特性。现在我们重写下GirlActor的代码
我们使用枚举来定义当前这个演员的状态,站立,和跑动状态,暂时我先定义两种,当然可以扩展为走,跳,飞,等各种状态。
演员的构造器中
Vector2 二维向量。具体二维向量的解释不介绍了。向量我们中学都学过(其实我也都忘了)。这里可以简单理解为,存储x,y坐标信息的一个东西。(向量在游戏开发中是很重要的一样概念。详细向量的用法,可以参考这篇文章。写的很棒,复习下我们的初中几何知识吧 http://blog.csdn.net/woyaowenzi/article/details/2501483)
接下来我们在view包中新建一个专门负责渲染这个演员的类。GirlView好了
前面我们合并了16张图片。这里我们使用到了资源加载管理的类。
AssetManager,这是专门用来处理资源加载的。例如纹理,音乐等等的加载。
想象一下,当一个大型游戏运行的时候,一般都有个进度条。大量资源的加载需要话费时间。
之所以我们这里感觉不到,是因为资源太小,太少了。否则会卡在一个黑屏阶段一段时间,等待资源加载完成,这无疑是不符合用户体验的。AssetManager却符合我们的要求。因为它能不断返回当前资源加载进度,是否加载完成的一些状态。这样我们就可以写出一个资源加载进度的进度条。
打开AssetManager的源码我们看下
所有支持的类型都在里边了。里边有我们前面用到的一些类型。如:Bit
mapFont,Texture等。
其中load方法可以添加我们需要加载的资源列表。
可以添加多个文件进行加载。第二个参数是我们加载资源的类型。
需要注意的是load只是添加了个需要加载的列表,实际还没有开始加载。
需要我们调用update方法
可以看出加载完成会返回一个boolean类型。
我们再看其中另外一个方法
相信都看的明白,这个就是返回当前加载进度的一个方法。
它返回的是一个,已加载/需要加载 的一个0-1之间的小数。当加载完成就是1了。
根据这个相信大家都能做出一个资源加载的进度条来。不停的根据加载进度修改某个纹理的长度就可以实现了。这里我就不实现了。文件太小,没啥效果。
接着看我们的代码
从加载完的资源中取出我们需要的图片资源。因为是一张整图。
我们需要分部位读取图片
这部分是默认向右的图片。
这里我们使用了一个libgdx的图片反转操作,省的我们再去做一组向左跑的图片。
挨个反转一下。好了,使用图片生成我们需要的左右跑动画就可以了。
接下来看下面的。
这里的render方法就是被不停执行的方法。等会我们会在MyGame的render方法调用这个方法。其实很简单
如果资源加载完成,那么我们就开始显示演员。isload只是我用来控制只执行一次loadAssets方法,当然可以去掉那个if,那就会不停执行了。为了节省资源。我只让它执行一次。
下面的就不解释了。就是判断当前演员的状态来显示是跑动动画还是站立不动的图片。
因为没有站立的图片,我就用第一张图当站立的好了
好了,修改下MyGame方法
MyGame类简单很多。只是初始化下参数就好了。
这才符合MVC嘛。但是这个小人还是只会站立不动。为啥呢。
忘了更新演员stateTime了。
记得上次解释帧率概念的那段代码吧GirlActor演员类中这个方法
现在给演员类加上更新时间的代码。在GirlActor中增加如下代码
因为需要不停更新。我们在GirlView中的方法中调用下它并赋值给当前帧间隔时间。
这样我们才能在这里获取到它的不停更新的statetime
现在运行下,动起来了。
修改下演员的 State state = State.RUNING;状态为IDLE就不会动了。
修改下位置信息就会改变当前位置。
这下明白了吧,只需要根据我们的需要修改演员的状态信息和位置信息它就可以被我们控制了
也就是我现在正在学习的control了。下次看看如何控制人物的走动。
本篇源码:http://pan.baidu.com/s/1nt4WNiD
在开始之前需要做些准备工作。人物图片一共有16张。如果循环加载无疑会加重系统负担。
这还只是一个人物,试想如果一个游戏中有好多的人物,那肯定会影响游戏运行。并且资源的加载方式也需要改进。
web的前端开发中会把使用到的图片合并。这样利于加载。libgdx也可以实现这种效果。我们先利用libgdx提供的工具合并下图片。让它变成一张图片。
进入libgdx源码,定位到我们编译出的dist文件夹。(从网上下载的官方压缩好的压缩包的,直接解压,进入根目录就可以了。)新建一个in文件夹,新建一个out文件夹 拷贝16张图片到in文件夹。然后使用命令行进入到dist文件夹根目录(下载压缩包的,直接进入解压后目录)
输入如下命令
java -cp gdx.jar;extensions/gdx-tools/gdx-tools.jar com.badlogic.gdx.tools.texturepacker.TexturePacker in out girlRun
好了,拷贝生成的两个文件到我们的项目assets目录,删除掉原来的16张。
现在思考下这个游戏,其实不算游戏,只是一个小人在跑动而已。
如何让游戏更加真实呢。比如人跑动的速度,与屏幕大小的关系。如果更准确的表述出来呢。
这就需要我们对整个游戏世界有个把握。要知道手机屏幕的大小是不确定的。同样我们制做的纹理如果要考虑到大小问题。想想一个大型游戏,上百张图片,我们都要控制好它的大小。与实际游戏屏幕进行匹配,是不是很头疼,我们这个游戏中人物太小了。如果要调整大小,我只能调整纹理,或者调整渲染时的大小。当然有更好的办法。我们用比例来理解屏幕。
下面我在actor包中新建一个world类,来更好的控制我们的世界
package com.me.mygdxgame.actor; import com.badlogic.gdx.utils.ArrayMap; /** * 我们的游戏世界 */ public class World<T> { //相当于我们游戏世界的大小。只是为了方便控制人物大小,移动速度等 //手机屏幕划分为10*6方格,具体每个方格大小不知道 private final float WORLD_WIDTH = 10f; private final float WORLD_HEIGHT = 6f; //也就是把屏幕分成10*6网格后每个单元格的x,y轴的像素数 //下面的方法中会根据屏幕大小自动计算出 private float pixelsX; private float pixelsY; private int width; private int height; public float getWorldWidth() { return WORLD_WIDTH; } public float getWorldHeight() { return WORLD_HEIGHT; } /** * 屏幕的实际宽度 * * @return width */ public int getWidth() { return width; } /** * 屏幕的实际高度 * * @return height */ public int getHeight() { return height; } //计算出每个方格的像素数 //例如800*480的屏幕 800/10 480/6 每单位x,y像素为80的方格 //这样方便我们控制世界中的物体的大小和比例 public void setPixelsXY(int width, int height) { this.width = width; this.height = height; pixelsX = (float) width / WORLD_WIDTH; pixelsY = (float) height / WORLD_HEIGHT; } public float getPixelsX() { return this.pixelsX; } public float getPixelsY() { return this.pixelsY; } ArrayMap<String, T> actors = new ArrayMap<String, T>(); public void addActors(String name, T actor) { actors.put(name, actor); } public T getActors(String name) { return actors.get(name); } }
为了方便理解上面的代码我专门做了一张图片
我们看上图,假设 屏幕分辨率为800*480 。我们分成10*6的方格。每个方格大小就是80*80像素大小的正方形。也就是我们使用setPixelsXY(int width, int height) 方法计算出的pixelsX,pixelsY的大小。
那么我们以后定义演员大小,速度等的时候就可以很直观的使用比例来定义。
例如我们定义演员为2f,也就是两个单位的方格。在渲染人物的时候我们直接使用演员的大小
size*pixels 这就是实际演员的像素大小。这样就很方便的定义游戏中的物体大小。和进行边界检查,碰撞等操作。我们没必要关心纹理大小和屏幕大小,只需要知道我们的游戏世界中的大小为10*6的,根据这个大小来调整演员等等物体的大小就可以了。很方便不是。
前面我们的演员代码并没有体现出这个演员的大小位置等等特性。现在我们重写下GirlActor的代码
package com.me.mygdxgame.actor; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class GirlActor { //定义一个演员状态的枚举方法 //这里我们只定义两种状态,站立状态,跑动状态 public enum State { IDLE, RUNING } //演员的位置 Vector2 position = new Vector2(); //这个演员的大小,因为纹理是方形或长方形的 Rectangle bounds = new Rectangle(); //默认状态是跑动的 State state = State.RUNING; float stateTime = 0; //演员的朝向 boolean facingRight = true; /** * 演员的构造器 * 我们初始化的时候需要指定演员的位置和大小 * * @param position 位置 * @param size 演员大小 */ public GirlActor(Vector2 position, Vector2 size) { this.position = position; //演员的大小 this.bounds.width = size.x; this.bounds.height = size.y; } //get set 方法,是不是很像我们的JavaBean public Vector2 getPosition() { return position; } public void setPosition(Vector2 position) { this.position = position; } public Rectangle getBounds() { return bounds; } public void setBounds(Rectangle bounds) { this.bounds = bounds; } public State getState() { return state; } public void setState(State state) { this.state = state; } public float getStateTime() { return stateTime; } public void setStateTime(float stateTime) { this.stateTime = stateTime; } public boolean isFacingRight() { return facingRight; } public void setFacingRight(boolean facingRight) { this.facingRight = facingRight; } }
public enum State { IDLE, RUNING }
我们使用枚举来定义当前这个演员的状态,站立,和跑动状态,暂时我先定义两种,当然可以扩展为走,跳,飞,等各种状态。
演员的构造器中
Vector2 position, Vector2 size参数第一个是位置,第二个是大小。当然你也可以直接在GirlActor中定义一个这个演员固定的大小。
Vector2 二维向量。具体二维向量的解释不介绍了。向量我们中学都学过(其实我也都忘了)。这里可以简单理解为,存储x,y坐标信息的一个东西。(向量在游戏开发中是很重要的一样概念。详细向量的用法,可以参考这篇文章。写的很棒,复习下我们的初中几何知识吧 http://blog.csdn.net/woyaowenzi/article/details/2501483)
接下来我们在view包中新建一个专门负责渲染这个演员的类。GirlView好了
package com.me.mygdxgame.view; import com.badlogic.gdx.assets.AssetManager; import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.utils.Disposable; import com.me.mygdxgame.actor.GirlActor; import com.me.mygdxgame.actor.World; /** * view层 * 主要负责渲染场景,演员和资源加载 */ public class GirlView implements Disposable { private World world; private GirlActor girlsActor; //资源管理器专门用来管理资源加载 AssetManager assetManager; private SpriteBatch spriteBatch; private TextureAtlas textureAtlas; private TextureRegion[] girlRunFrameLeft; private TextureRegion[] girlRunFrameRight; private Animation animationRight; private Animation animationLeft; private TextureRegion currentFrame; private boolean isload; /** * 构造方法 初始化相关的参数。加载演员资源 * * @param world 游戏世界 */ public GirlView(World world) { assetManager = new AssetManager(); assetManager.load("data/image/girlRun.atlas", TextureAtlas.class); this.world = world; girlsActor = (GirlActor) world.getActors("girl"); spriteBatch = new SpriteBatch(); currentFrame = new TextureRegion(); girlRunFrameLeft = new TextureRegion[16]; girlRunFrameRight = new TextureRegion[16]; isload = false; } /** * 演员资源加载 */ private void loadAssets() { //使用资源管理器加载图片纹理资源 textureAtlas = assetManager.get("data/image/girlRun.atlas", TextureAtlas.class); //面朝右边跑动的纹理资源 for (int i = 0; i < 16; i++) { girlRunFrameRight[i] = textureAtlas.findRegion(String.valueOf(i + 1)); } //面朝左边跑动的纹理资源 for (int i = 0; i < 16; i++) { girlRunFrameLeft[i] = new TextureRegion(girlRunFrameRight[i]); //把图片进行反转,第一个是x,第二个是y,我们只进行x方向上的反转,如果进行y反转,人物就倒立了。 girlRunFrameLeft[i].flip(true, false); } //演员左右跑动的动画 animationLeft = new Animation(0.06f, girlRunFrameLeft); animationRight = new Animation(0.06f, girlRunFrameRight); isload = true; } /** * 演员显示 */ public void render() { if (assetManager.update()) { if (!isload) { loadAssets(); } spriteBatch.begin(); drawActors(); spriteBatch.end(); } } /** * 演员的渲染逻辑 */ private void drawActors() { //获取演员的当前状态,是站立还是跑动状态 switch (girlsActor.getState()) { //站立状态 case IDLE: //根据演员的当前面朝方向获取演员的纹理。因为图片中没有站立的,所以我们取第一个为站立图片使用 currentFrame = girlsActor.isFacingRight() ? girlRunFrameRight[0] : girlRunFrameLeft[0]; break; //跑动状态,当然是获取跑动动画了 case RUNING: currentFrame = girlsActor.isFacingRight() ? animationRight.getKeyFrame(girlsActor.getStateTime(), true) : animationLeft.getKeyFrame(girlsActor.getStateTime(), true); break; default: break; } spriteBatch.draw(currentFrame , girlsActor.getPosition().x * world.getPixelsX() , girlsActor.getPosition().y * world.getPixelsY() , girlsActor.getBounds().getWidth() * world.getPixelsX() , girlsActor.getBounds().getHeight() * world.getPixelsY()); } /** * 资源回收 */ @Override public void dispose() { assetManager.dispose(); textureAtlas.dispose(); spriteBatch.dispose(); } }
前面我们合并了16张图片。这里我们使用到了资源加载管理的类。
AssetManager,这是专门用来处理资源加载的。例如纹理,音乐等等的加载。
想象一下,当一个大型游戏运行的时候,一般都有个进度条。大量资源的加载需要话费时间。
之所以我们这里感觉不到,是因为资源太小,太少了。否则会卡在一个黑屏阶段一段时间,等待资源加载完成,这无疑是不符合用户体验的。AssetManager却符合我们的要求。因为它能不断返回当前资源加载进度,是否加载完成的一些状态。这样我们就可以写出一个资源加载进度的进度条。
打开AssetManager的源码我们看下
/** Creates a new AssetManager with all default loaders. */ public AssetManager (FileHandleResolver resolver) { setLoader(BitmapFont.class, new BitmapFontLoader(resolver)); setLoader(Music.class, new MusicLoader(resolver)); setLoader(Pixmap.class, new PixmapLoader(resolver)); setLoader(Sound.class, new SoundLoader(resolver)); setLoader(TextureAtlas.class, new TextureAtlasLoader(resolver)); setLoader(Texture.class, new TextureLoader(resolver)); setLoader(Skin.class, new SkinLoader(resolver)); setLoader(ParticleEffect.class, new ParticleEffectLoader(resolver)); setLoader(Model.class, ".g3dj", new G3dModelLoader(new JsonReader(), resolver)); setLoader(Model.class, ".g3db", new G3dModelLoader(new UBJsonReader(), resolver)); setLoader(Model.class, ".obj", new ObjLoader(resolver)); executor = new AsyncExecutor(1); }
所有支持的类型都在里边了。里边有我们前面用到的一些类型。如:Bit
mapFont,Texture等。
其中load方法可以添加我们需要加载的资源列表。
assetManager.load("data/image/girlRun.atlas", TextureAtlas.class);
可以添加多个文件进行加载。第二个参数是我们加载资源的类型。
需要注意的是load只是添加了个需要加载的列表,实际还没有开始加载。
需要我们调用update方法
/** Updates the AssetManager, keeping it loading any assets in the preload queue. * @return true if all loading is finished. */ public synchronized boolean update () { try { if (tasks.size() == 0) { // loop until we have a new task ready to be processed while (loadQueue.size != 0 && tasks.size() == 0) { nextTask(); } // have we not found a task? We are done! if (tasks.size() == 0) return true; } return updateTask() && loadQueue.size == 0 && tasks.size() == 0; } catch (Throwable t) { handleTaskError(t); return loadQueue.size == 0; } }
可以看出加载完成会返回一个boolean类型。
我们再看其中另外一个方法
/** @return the progress in percent of completion. */ public synchronized float getProgress () { if (toLoad == 0) return 1; return Math.min(1, loaded / (float)toLoad); }
相信都看的明白,这个就是返回当前加载进度的一个方法。
它返回的是一个,已加载/需要加载 的一个0-1之间的小数。当加载完成就是1了。
根据这个相信大家都能做出一个资源加载的进度条来。不停的根据加载进度修改某个纹理的长度就可以实现了。这里我就不实现了。文件太小,没啥效果。
接着看我们的代码
private void loadAssets() { //使用资源管理器加载图片纹理资源 textureAtlas = assetManager.get("data/image/girlRun.atlas", TextureAtlas.class); //面朝右边跑动的纹理资源 for (int i = 0; i < 16; i++) { girlRunFrameRight[i] = textureAtlas.findRegion(String.valueOf(i + 1)); } //面朝右边跑动的纹理资源 for (int i = 0; i < 16; i++) { girlRunFrameLeft[i] = new TextureRegion(girlRunFrameRight[i]); //把图片进行反转,第一个是x,第二个是y,我们只进行x方向上的反转,如果进行y反转,人物就倒立了。 girlRunFrameLeft[i].flip(true, false); } //演员左右跑动的动画 animationLeft = new Animation(0.06f, girlRunFrameLeft); animationRight = new Animation(0.06f, girlRunFrameRight); isload = true; }
textureAtlas = assetManager.get("data/image/girlRun.atlas", TextureAtlas.class);
从加载完的资源中取出我们需要的图片资源。因为是一张整图。
我们需要分部位读取图片
girlRunFrameRight[i] = textureAtlas.findRegion(String.valueOf(i + 1));
这部分是默认向右的图片。
这里我们使用了一个libgdx的图片反转操作,省的我们再去做一组向左跑的图片。
girlRunFrameLeft[i] = new TextureRegion(girlRunFrameRight[i]); //把图片进行反转,第一个是x,第二个是y,我们只进行x方向上的反转,如果进行y反转,人物就倒立了。 girlRunFrameLeft[i].flip(true, false);
挨个反转一下。好了,使用图片生成我们需要的左右跑动画就可以了。
接下来看下面的。
/** * 演员显示 */ public void render() { if (assetManager.update()) { if (!isload) { loadAssets(); } spriteBatch.begin(); drawActors(); spriteBatch.end(); } }
这里的render方法就是被不停执行的方法。等会我们会在MyGame的render方法调用这个方法。其实很简单
如果资源加载完成,那么我们就开始显示演员。isload只是我用来控制只执行一次loadAssets方法,当然可以去掉那个if,那就会不停执行了。为了节省资源。我只让它执行一次。
下面的就不解释了。就是判断当前演员的状态来显示是跑动动画还是站立不动的图片。
因为没有站立的图片,我就用第一张图当站立的好了
好了,修改下MyGame方法
package com.me.mygdxgame; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.math.Vector2; import com.me.mygdxgame.actor.GirlActor; import com.me.mygdxgame.actor.World; import com.me.mygdxgame.view.GirlView; public class MyGame implements ApplicationListener { private World<GirlActor> world; private GirlView girlView; @Override public void create() { world = new World<GirlActor>(); GirlActor girlsActor = new GirlActor(new Vector2(0, 0), new Vector2(2, 2)); world.addActors("girl", girlsActor); girlView = new GirlView(world); } @Override public void render() { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); Gdx.gl.glClearColor(0.57f, 0.41f, 0.55f, 1.0f); girlView.render(); } @Override public void dispose() { girlView.dispose(); } @Override public void resize(int width, int height) { world.setPixelsXY(width, height); } @Override public void pause() { } @Override public void resume() { } }
MyGame类简单很多。只是初始化下参数就好了。
这才符合MVC嘛。但是这个小人还是只会站立不动。为啥呢。
忘了更新演员stateTime了。
记得上次解释帧率概念的那段代码吧GirlActor演员类中这个方法
@Override public void draw(Batch batch, float parentAlpha) { System.out.println("DeltaTime====="+Gdx.graphics.getDeltaTime()); stateTime += Gdx.graphics.getDeltaTime(); //下一帧 currentFrame = animation.getKeyFrame(stateTime, true); //绘制人物 batch.draw(currentFrame, 0, 0);
现在给演员类加上更新时间的代码。在GirlActor中增加如下代码
public void update(float deltaTime) { stateTime += deltaTime; }
因为需要不停更新。我们在GirlView中的方法中调用下它并赋值给当前帧间隔时间。
/** * 演员的渲染逻辑 */ private void drawActors() { girlsActor.update(Gdx.graphics.getDeltaTime()); //获取演员的当前状态,是站立还是跑动状态 switch (girlsActor.getState()) { //站立状态 case IDLE: //根据演员的当前面朝方向获取演员的纹理。因为图片中没有站立的,所以我们取第一个为站立图片使用 currentFrame = girlsActor.isFacingRight() ? girlRunFrameRight[0] : girlRunFrameLeft[0]; break; //跑动状态,当然是获取跑动动画了 case RUNING: currentFrame = girlsActor.isFacingRight() ? animationRight.getKeyFrame(girlsActor.getStateTime(), true) : animationLeft.getKeyFrame(girlsActor.getStateTime(), true); break; default: break; } spriteBatch.draw(currentFrame , girlsActor.getPosition().x * world.getPixelsX() , girlsActor.getPosition().y * world.getPixelsY() , girlsActor.getBounds().getWidth() * world.getPixelsX() , girlsActor.getBounds().getHeight() * world.getPixelsY()); }
这样我们才能在这里获取到它的不停更新的statetime
currentFrame = girlsActor.isFacingRight() ? animationRight.getKeyFrame(girlsActor.getStateTime(), true) : animationLeft.getKeyFrame(girlsActor.getStateTime(), true);
现在运行下,动起来了。
修改下演员的 State state = State.RUNING;状态为IDLE就不会动了。
修改下位置信息就会改变当前位置。
这下明白了吧,只需要根据我们的需要修改演员的状态信息和位置信息它就可以被我们控制了
也就是我现在正在学习的control了。下次看看如何控制人物的走动。
本篇源码:http://pan.baidu.com/s/1nt4WNiD
上一篇: nginx限速之连接数限制详解