提要
前面已经让大家学习安卓游戏开发的基础,那么下面我们就可以开始开发一个Android游戏了,当然,现阶段只是学习为主。
下面要做的一个游戏叫做 Star Guard,一款非常棒的独立游戏,画面非常有爱,难度不小,不过有无限生命可以玩。
键盘的上下左右控制小人,x开火,z跳跃。
今天我们要做的就是搭建舞台,创建项目的骨架。
最终的效果看起来是这样:
当然,可以同时部署在手机和PC上。
搭建工程
偷懒的话直接用上次创建的工程就ok,如果想重新创建的话,用gdx-setup-ui也很快。
在eclipse中导入三个工程。
看起来就像这样:
我们主要编写的工程是test-gdx-game.其他两个基本不东。
在工程下面创建相应的包,包括controller,model,screens,view.
这里用到了一些MVC的知识,简单提一下:
MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组:
(控制器Controller)- 负责转发请求,对请求进行处理。
(视图View) - 界面设计人员进行图形界面设计。
(模型Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
编码
model这里有三个model需要创建-Block(砖块),Bob(人物),World(世界)。
Block.java
package com.me.testgdxgame.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Block { public static final float SIZE = 1f; Vector2 position = new Vector2(); Rectangle bounds = new Rectangle(); public Block(Vector2 pos) { this.position = pos; this.bounds.setX(pos.x); this.bounds.setY(pos.y); this.bounds.width = SIZE; this.bounds.height = SIZE; } public Vector2 getPosition() { return position; } public Rectangle getBounds() { return bounds; } }Vector2是libgdx中的二维向量类。bounds用于后面的碰撞检测。
Bob.java
package com.me.testgdxgame.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Bob { public enum State { IDLE, WALKING, JUMPING, DYING } public static final float SPEED = 4f; static final float JUMP_VELOCITY = 1f; public static final float SIZE = 0.5f; Vector2 position = new Vector2(); Vector2 acceleration = new Vector2(); Vector2 velocity = new Vector2(); Rectangle bounds = new Rectangle(); State state = State.IDLE; boolean facingLeft = true; float stateTime = 0; public Bob(Vector2 position) { this.position = position; this.bounds.height = SIZE; this.bounds.width = SIZE; } public boolean isFacingLeft() { return facingLeft; } public void setFacingLeft(boolean facingLeft) { this.facingLeft = facingLeft; } public Vector2 getPosition() { return position; } public Vector2 getAcceleration() { return acceleration; } public Vector2 getVelocity() { return velocity; } public Rectangle getBounds() { return bounds; } public State getState() { return state; } public void setState(State newState) { this.state = newState; } public float getStateTime() { return stateTime; } public void update(float delta) { position.add(velocity.cpy().mul(delta)); } }Bob就有很多属性了,速度,位置什么的,代码也很简单。
World.java
package com.me.testgdxgame.model; import java.util.ArrayList; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; public class World { ArrayList blocks = new ArrayList(); Bob bob; public ArrayList getBlocks() { return blocks; } public Bob getBob() { return bob; } public World() { createDemoWorld(); } private void createDemoWorld() { bob = new Bob(new Vector2(7, 2)); for (int i = 0; i < 10; i ) { blocks.add(new Block(new Vector2(i, 0))); blocks.add(new Block(new Vector2(i, 6))); if (i > 2) blocks.add(new Block(new Vector2(i, 1))); } blocks.add(new Block(new Vector2(9, 2))); blocks.add(new Block(new Vector2(9, 3))); blocks.add(new Block(new Vector2(9, 4))); blocks.add(new Block(new Vector2(9, 5))); blocks.add(new Block(new Vector2(6, 3))); blocks.add(new Block(new Vector2(6, 4))); blocks.add(new Block(new Vector2(6, 5))); } }World中包括了地图和人物。
Viewview中的类主要负责渲染。
首先在项目中添加两个纹理。
放到android工程的asset/data下面就可以了。
注意:纹理贴图的长宽像素一定是2的幂,比如64×64,128×128..否则无法加载。官方解释是opengl的一个bug,无法解决。
package com.me.testgdxgame.view; import com.me.testgdxgame.*; import com.me.testgdxgame.model.Block; import com.me.testgdxgame.model.Bob; import com.me.testgdxgame.model.World; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private World world; private OrthographicCamera cam; private SpriteBatch spriteBatch; private boolean debug=false; private int width; private int height; private float ppuX; private float ppuY; private static final float CAMERA_WIDTH = 10f; private static final float CAMERA_HEIGHT = 7f; private Texture bobTexture; private Texture blockTexture; ShapeRenderer debugRenderer = new ShapeRenderer(); public WorldRenderer(World world) { this.world = world; this.cam = new OrthographicCamera(10, 7); this.cam.position.set(5, 3.5f, 0); this.cam.update(); spriteBatch=new SpriteBatch(); loadTextures(); } public void setSize (int w, int h) { this.width = w; this.height = h; ppuX = (float)width / CAMERA_WIDTH; ppuY = (float)height / CAMERA_HEIGHT; } private void loadTextures(){ bobTexture=new Texture(Gdx.files.internal("data/bob_01.png")); blockTexture=new Texture(Gdx.files.internal("data/block.png")); } public void render() { spriteBatch.begin(); drawBlocks(); drawBob(); spriteBatch.end(); if(debug) drawDebug(); } private void drawBlocks(){ for (Block block : world.getBlocks()) { spriteBatch.draw(blockTexture, block.getPosition().x * ppuX, block.getPosition().y * ppuY, Block.SIZE * ppuX, Block.SIZE * ppuY); } } private void drawBob(){ Bob bob = world.getBob(); spriteBatch.draw(bobTexture, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.SIZE * ppuX, Bob.SIZE * ppuY); } private void drawDebug(){ debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Rectangle); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x rect.x; float y1 = block.getPosition().y rect.y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x rect.x; float y1 = bob.getPosition().y rect.y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } }有几个类简单解释一下:
OrthographicCamera:正交坐标系Camerta。用于设置摄像机。到后期需要将人固定在摄像机视口中间,跟随人移动。
ShapeRenderer:用于绘制,包括纹理,线条,点,矩形等等。
SpriteBatch:负责加载,管理,绘制纹理。
这个类里面有个drawDebug()可以用于不添加纹理的时候绘制,绘制的结果如下:
这个类用于处理输入。
WorldController.java
package com.me.testgdxgame.controller; import java.util.HashMap; import java.util.Map; import com.me.testgdxgame.model.Bob; import com.me.testgdxgame.model.Bob.State; import com.me.testgdxgame.model.World; public class WorldController { enum Keys{ LEFT,RIGHT,JUMP,FIRE } private World world; private Bob bob; static Map keys = new HashMap(); static { keys.put(Keys.LEFT, false); keys.put(Keys.RIGHT, false); keys.put(Keys.JUMP, false); keys.put(Keys.FIRE, false); }; public WorldController(World w){ world=w; bob=world.getBob(); } public void leftPressed(){ keys.get(keys.put(Keys.LEFT, true)); } public void rightPressed() { keys.get(keys.put(Keys.RIGHT, true)); } public void jumpPressed() { keys.get(keys.put(Keys.JUMP, true)); } public void firePressed() { keys.get(keys.put(Keys.FIRE, false)); } public void leftReleased() { keys.get(keys.put(Keys.LEFT, false)); } public void rightReleased() { keys.get(keys.put(Keys.RIGHT, false)); } public void jumpReleased() { keys.get(keys.put(Keys.JUMP, false)); } public void fireReleased() { keys.get(keys.put(Keys.FIRE, false)); } public void update(float delta){ processInput(); bob.update(delta); } private void processInput(){ if(keys.get(Keys.LEFT)){ bob.setFacingLeft(true); bob.setState(State.WALKING); bob.getVelocity().x=-Bob.SPEED; } if (keys.get(Keys.RIGHT)) { bob.setFacingLeft(false); bob.setState(State.WALKING); bob.getVelocity().x = Bob.SPEED; } if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) || (!keys.get(Keys.LEFT) && !(keys.get(Keys.RIGHT)))) { bob.setState(State.IDLE); bob.getAcceleration().x = 0; bob.getVelocity().x = 0; } } }屏幕
mvc都有了,接下来就可以屏幕了。
一个游戏通常有很多屏幕,欢迎屏幕,游戏屏幕,参数设置屏幕,游戏结束屏幕等等。
今天我们之做一个游戏屏幕。
在screens包里添加GameScreen类。
package com.me.testgdxgame.screens; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL10; import com.me.testgdxgame.controller.WorldController; import com.me.testgdxgame.model.World; import com.me.testgdxgame.view.WorldRenderer; public class GameScreen implements Screen ,InputProcessor{ private WorldRenderer renderer; private World world; private WorldController controller; private int width, height; @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); controller.update(delta); renderer.render(); } @Override public void resize(int width, int height) { renderer.setSize(width, height); this.width=width; this.height=height; } @Override public void show() { world = new World(); renderer = new WorldRenderer(world); controller=new WorldController(world); Gdx.input.setInputProcessor(this); } @Override public void hide() { Gdx.input.setInputProcessor(null); } @Override public void pause() { } @Override public void resume() { } @Override public void dispose() { Gdx.input.setInputProcessor(null); } @Override public boolean keyDown(int keycode) { if (keycode == Keys.LEFT) controller.leftPressed(); if (keycode == Keys.RIGHT) controller.rightPressed(); if (keycode == Keys.Z) controller.jumpPressed(); if (keycode == Keys.X) controller.firePressed(); return true; } @Override public boolean keyUp(int keycode) { if (keycode == Keys.LEFT) controller.leftReleased(); if (keycode == Keys.RIGHT) controller.rightReleased(); if (keycode == Keys.Z) controller.jumpReleased(); if (keycode == Keys.X) controller.fireReleased(); return true; } @Override public boolean keyTyped(char character) { return false; } @Override public boolean touchDown(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftPressed(); } if (x > width / 2 && y > height / 2) { controller.rightPressed(); } return false; } @Override public boolean touchUp(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftReleased(); } if (x > width / 2 && y > height / 2) { controller.rightReleased(); } return true; } @Override public boolean touchDragged(int screenX, int screenY, int pointer) { return false; } @Override public boolean mouseMoved(int screenX, int screenY) { return false; } @Override public boolean scrolled(int amount) { return false; } }这个类实现了Screen ,InputProcessor接口。
InputProcessor用于接受触控事件和按键事件。
部署
desktop版本的项目基本不用修改。main函数如下:
public class Main { public static void main(String[] args) { LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration(); cfg.title = "test-gdx-game"; cfg.useGL20 = false; cfg.width = 1024; cfg.height = 600; new LwjglApplication(new TestGdxGame(), cfg); } }Main.java上右击->run as->java application.
android版本修改一下MainActiviy:
public class MainActivity extends AndroidApplication { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration(); cfg.useGL20 = false; initialize(new TestGdxGame(), cfg); } }run as->android application.
总结
到现在,舞台和代码框架已经搭建好了,后面还可以添加下面的东西:
.地行碰撞
.动画
.高等级的camera(视角可以不断变化)
.音效
.改进的输入
.更多的GameScreen
一起期待下一篇教程。