声明:《使用Libgdx进行游戏开发》是一个系列,文章的英文原文是《LearningLibgdx Game Development》,大家请周知。所有文章连接在这里
[libgdx游戏开发教程]使用Libgdx进行游戏开发(1)-游戏设计
[libgdx游戏开发教程]使用Libgdx进行游戏开发(2)-游戏框架搭建
[libgdx游戏开发教程]使用Libgdx进行游戏开发(3)-给游戏添加一些控制功能
[libgdx游戏开发教程]使用Libgdx进行游戏开发(4)-素材管理
[libGDX游戏开发教程]使用Libgdx进行游戏开发(5)-关卡加载
[libgdx游戏开发教程]使用Libgdx进行游戏开发(6)-添加主角和道具
[libgdx游戏开发教程]使用Libgdx进行游戏开发(7)-屏幕布局的最佳实践
[libgdx游戏开发教程]使用Libgdx进行游戏开发(8)-没有美工的程序员,能够依赖的还有粒子系统
[libgdx游戏开发教程]使用Libgdx进行游戏开发(9)-场景过渡
[libgdx游戏开发教程]使用Libgdx进行游戏开发(10)-音乐音效不求人,程序员也可以DIY
[libgdx游戏开发教程]使用Libgdx进行游戏开发(11)-高级编程技巧
[libGDX游戏开发教程]使用libGDX进行游戏开发(12)-动画
本章素材:http://files.cnblogs.com/mignet/assets.zip
在上一章我们介绍了如何管理和利用素材,但是我们注意到,这些素材都是零散的,比如岩石的左部等,这一章,我们将利用这些零件拼合成完整的游戏对象。
回顾最开始的设计类图,注意Level类和所有Level中的Object,看看它们的继承关系。
首先第一步就是创建所有对象的基类AbstractGameObject.
它应该包含所有公共的属性和功能。
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Vector2; public abstract class AbstractGameObject { public Vector2 position; public Vector2 dimension; public Vector2 origin; public Vector2 scale; public float rotation; public AbstractGameObject() { position = new Vector2(); dimension = new Vector2(1, 1); origin = new Vector2(); scale = new Vector2(1, 1); rotation = 0; } public void update(float deltaTime) { } public abstract void render(SpriteBatch batch); }
这个抽象类包含很多基本的属性,update和render。update更新自己,render画自己。很多人虽然知道OOP,但是并没有在思维中形成OO的观念。对象的划分以及对象的行为(或者说对象的权责)是否分明,都能看出你编程的功力。
render是abstract的,这就限定了所有的子类需要自己去实现它。
我们先看Rock,Rock是由3个部分组成的,左中右,中间的部分是能够重复的。像这样
那么它的实现类似于:
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.packtpub.libgdx.canyonbunny.game.Assets; public class Rock extends AbstractGameObject { private TextureRegion regEdge; private TextureRegion regMiddle; private int length; public Rock() { init(); } private void init() { dimension.set(1, 1.5f); regEdge = Assets.instance.rock.edge; regMiddle = Assets.instance.rock.middle; // Start length of this rock setLength(1); } public void setLength(int length) { this.length = length; } public void increaseLength(int amount) { setLength(length + amount); } @Override public void render(SpriteBatch batch) { TextureRegion reg = null; float relX = 0; float relY = 0; // Draw left edge reg = regEdge; relX -= dimension.x / 4; batch.draw(reg.getTexture(), position.x + relX, position.y + relY, origin.x, origin.y, dimension.x / 4, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); // Draw middle relX = 0; reg = regMiddle; for (int i = 0; i < length; i++) { batch.draw(reg.getTexture(), position.x + relX, position.y + relY, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); relX += dimension.x; } // Draw right edge reg = regEdge; batch.draw(reg.getTexture(), position.x + relX, position.y + relY, origin.x + dimension.x / 8, origin.y, dimension.x / 4, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), true, false); } }
我们使用了一个length来表示rock的长度,就是中间可以重复的部分。
接下来是山,有人可能会奇怪,为什么用白色的山呢?用白色是为了方便着色的。Mountains类似于:
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.MathUtils; import com.packtpub.libgdx.canyonbunny.game.Assets; public class Mountains extends AbstractGameObject { private TextureRegion regMountainLeft; private TextureRegion regMountainRight; private int length; public Mountains(int length) { this.length = length; init(); } private void init() { dimension.set(10, 2); regMountainLeft = Assets.instance.levelDecoration.mountainLeft; regMountainRight = Assets.instance.levelDecoration.mountainRight; // shift mountain and extend length origin.x = -dimension.x * 2; length += dimension.x * 2; } private void drawMountain(SpriteBatch batch, float offsetX, float offsetY, float tintColor) { TextureRegion reg = null; batch.setColor(tintColor, tintColor, tintColor, 1); float xRel = dimension.x * offsetX; float yRel = dimension.y * offsetY; // mountains span the whole level int mountainLength = 0; mountainLength += MathUtils.ceil(length / (2 * dimension.x)); mountainLength += MathUtils.ceil(0.5f + offsetX); for (int i = 0; i < mountainLength; i++) { // mountain left reg = regMountainLeft; batch.draw(reg.getTexture(), origin.x + xRel, position.y + origin.y + yRel, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); xRel += dimension.x; // mountain right reg = regMountainRight; batch.draw(reg.getTexture(), origin.x + xRel, position.y + origin.y + yRel, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); xRel += dimension.x; } // reset color to white batch.setColor(1, 1, 1, 1); } @Override public void render(SpriteBatch batch) { // distant mountains (dark gray) drawMountain(batch, 0.5f, 0.5f, 0.5f); // distant mountains (gray) drawMountain(batch, 0.25f, 0.25f, 0.7f); // distant mountains (light gray) drawMountain(batch, 0.0f, 0.0f, 0.9f); } }
这个跟Rock很像,也用了一个length来存储需要重复的次数。在render里调用了3个不同的drawMountain,这样大大的简化了画3层山的代码。
接下来是水面,这个类要比前面的简单多了,它只需要沿着x轴拉伸造成一直存在的假象就行了。(还有很多其他的方法可以达到这个目的:比如用一个摄像机视口一样宽的图片,跟着摄像机一起移动。不过这样你需要小心摄像机可能垂直移动)
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.packtpub.libgdx.canyonbunny.game.Assets; public class WaterOverlay extends AbstractGameObject { private TextureRegion regWaterOverlay; private float length; public WaterOverlay(float length) { this.length = length; init(); } private void init() { dimension.set(length * 10, 3); regWaterOverlay = Assets.instance.levelDecoration.waterOverlay; origin.x = -dimension.x / 2; } @Override public void render(SpriteBatch batch) { TextureRegion reg = null; reg = regWaterOverlay; batch.draw(reg.getTexture(), position.x + origin.x, position.y + origin.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); } }
接下来是云彩,云彩的分布由长度和间距两个参数决定。
package com.packtpub.libgdx.canyonbunny.game.objects; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; import com.packtpub.libgdx.canyonbunny.game.Assets; public class Clouds extends AbstractGameObject { private float length; private Array<TextureRegion> regClouds; private Array<Cloud> clouds; private class Cloud extends AbstractGameObject { private TextureRegion regCloud; public Cloud() { } public void setRegion(TextureRegion region) { regCloud = region; } @Override public void render(SpriteBatch batch) { TextureRegion reg = regCloud; batch.draw(reg.getTexture(), position.x + origin.x, position.y + origin.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); } } public Clouds(float length) { this.length = length; init(); } private void init() { dimension.set(3.0f, 1.5f); regClouds = new Array<TextureRegion>(); regClouds.add(Assets.instance.levelDecoration.cloud01); regClouds.add(Assets.instance.levelDecoration.cloud02); regClouds.add(Assets.instance.levelDecoration.cloud03); int distFac = 5; int numClouds = (int) (length / distFac); clouds = new Array<Cloud>(2 * numClouds); for (int i = 0; i < numClouds; i++) { Cloud cloud = spawnCloud(); cloud.position.x = i * distFac; clouds.add(cloud); } } private Cloud spawnCloud() { Cloud cloud = new Cloud(); cloud.dimension.set(dimension); // select random cloud image cloud.setRegion(regClouds.random()); // position Vector2 pos = new Vector2(); pos.x = length + 10; // position after end of level pos.y += 1.75; // base position // random additional position pos.y += MathUtils.random(0.0f, 0.2f) * (MathUtils.randomBoolean() ? 1 : -1); cloud.position.set(pos); return cloud; } @Override public void render(SpriteBatch batch) { for (Cloud cloud : clouds) cloud.render(batch); } }
Clouds定义了内部类Cloud,Clouds是包含云彩的容器。
关卡加载
我们使用png图片来保存关卡数据:1像素代表1个对象,每一种不同的对象都有一种唯一的RGBA颜色值。我们使用纯色,不用透明色,那么一个RGBA就是32位,就是4字节。刚好java的int也是32位,用来存颜色刚刚好。
我们需要读取并解析它们:
package com.packtpub.libgdx.canyonbunny.game; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.utils.Array; import com.packtpub.libgdx.canyonbunny.game.objects.AbstractGameObject; import com.packtpub.libgdx.canyonbunny.game.objects.Clouds; import com.packtpub.libgdx.canyonbunny.game.objects.Mountains; import com.packtpub.libgdx.canyonbunny.game.objects.Rock; import com.packtpub.libgdx.canyonbunny.game.objects.WaterOverlay; public class Level { public static final String TAG = Level.class.getName(); public enum BLOCK_TYPE { EMPTY(0, 0, 0), // black ROCK(0, 255, 0), // green PLAYER_SPAWNPOINT(255, 255, 255), // white ITEM_FEATHER(255, 0, 255), // purple ITEM_GOLD_COIN(255, 255, 0); // yellow private int color; private BLOCK_TYPE(int r, int g, int b) { color = r << 24 | g << 16 | b << 8 | 0xff; } public boolean sameColor(int color) { return this.color == color; } public int getColor() { return color; } } // objects public Array<Rock> rocks; // decoration public Clouds clouds; public Mountains mountains; public WaterOverlay waterOverlay; public Level(String filename) { init(filename); } private void init(String filename) { } public void render(SpriteBatch batch) { } }
在init中加入代码 读地图,然后解析:(分析过tiledmap的同学可能知道,这个步骤在使用tiledmap时也是一样的过程)
private void init(String filename) { // objects rocks = new Array<Rock>(); // load image file that represents the level data Pixmap pixmap = new Pixmap(Gdx.files.internal(filename)); // scan pixels from top-left to bottom-right int lastPixel = -1; for (int pixelY = 0; pixelY < pixmap.getHeight(); pixelY++) { for (int pixelX = 0; pixelX < pixmap.getWidth(); pixelX++) { AbstractGameObject obj = null; float offsetHeight = 0; // height grows from bottom to top float baseHeight = pixmap.getHeight() - pixelY; // get color of current pixel as 32-bit RGBA value int currentPixel = pixmap.getPixel(pixelX, pixelY); // find matching color value to identify block type at (x,y) // point and create the corresponding game object if there is // a match // empty space if (BLOCK_TYPE.EMPTY.sameColor(currentPixel)) { // do nothing } // rock else if (BLOCK_TYPE.ROCK.sameColor(currentPixel)) { if (lastPixel != currentPixel) { obj = new Rock(); float heightIncreaseFactor = 0.25f; offsetHeight = -2.5f; obj.position.set(pixelX, baseHeight * obj.dimension.y * heightIncreaseFactor + offsetHeight); rocks.add((Rock) obj); } else { rocks.get(rocks.size - 1).increaseLength(1); } } // player spawn point else if (BLOCK_TYPE.PLAYER_SPAWNPOINT.sameColor(currentPixel)) { } // feather else if (BLOCK_TYPE.ITEM_FEATHER.sameColor(currentPixel)) { } // gold coin else if (BLOCK_TYPE.ITEM_GOLD_COIN.sameColor(currentPixel)) { } // unknown object/pixel color else { int r = 0xff & (currentPixel >>> 24); // red color channel int g = 0xff & (currentPixel >>> 16); // green color channel int b = 0xff & (currentPixel >>> 8); // blue color channel int a = 0xff & currentPixel; // alpha channel Gdx.app.error(TAG, "Unknown object at x<" + pixelX + "> y<" + pixelY + ">: r<" + r + "> g<" + g + "> b<" + b + "> a<" + a + ">"); } lastPixel = currentPixel; } } // decoration clouds = new Clouds(pixmap.getWidth()); clouds.position.set(0, 2); mountains = new Mountains(pixmap.getWidth()); mountains.position.set(-1, -1); waterOverlay = new WaterOverlay(pixmap.getWidth()); waterOverlay.position.set(0, -3.75f); // free memory pixmap.dispose(); Gdx.app.debug(TAG, "level '" + filename + "' loaded"); }
以此遍历渲染:
public void render(SpriteBatch batch) { // Draw Mountains mountains.render(batch); // Draw Rocks for (Rock rock : rocks) rock.render(batch); // Draw Water Overlay waterOverlay.render(batch); // Draw Clouds clouds.render(batch); }
渲染的次序决定了相互覆盖的效果。你可以想象它们是不同的层(当然实际上它们没有分层画,这个跟Unity不是一样的,但你可以这么以为),从45°角来看是这样的。
first to last,越后画的越显示在前边。
接下来,开始整合:
在Constants里加上一些游戏常量
public class Constants { // Visible game world is 5 meters wide public static final float VIEWPORT_WIDTH = 5.0f; // Visible game world is 5 meters tall public static final float VIEWPORT_HEIGHT = 5.0f; // GUI Width public static final float VIEWPORT_GUI_WIDTH = 800.0f; // GUI Height public static final float VIEWPORT_GUI_HEIGHT = 480.0f; // Location of description file for texture atlas public static final String TEXTURE_ATLAS_OBJECTS = "images/canyonbunny.pack"; // Location of image file for level 01 public static final String LEVEL_01 = "levels/level-01.png"; // Amount of extra lives at level start public static final int LIVES_START = 3; }
移除controller里的testSprites和selectedSprite;当然也要移除那些相应的方法initTestObjects(),updateTestObjects(),moveSelectedSprite()。
删除handleDebugInput()里WSAD的控制。KeyUp只保留R键。
package com.packtpub.libgdx.canyonbunny.game; import com.badlogic.gdx.Application.ApplicationType; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.InputAdapter; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; import com.packtpub.libgdx.canyonbunny.util.CameraHelper; import com.packtpub.libgdx.canyonbunny.util.Constants; public class WorldController extends InputAdapter { private static final String TAG = WorldController.class.getName(); public CameraHelper cameraHelper; public Level level; public int lives; public int score; private void initLevel() { score = 0; level = new Level(Constants.LEVEL_01); } public WorldController() { Gdx.input.setInputProcessor(this); init(); } private void handleDebugInput(float deltaTime) { if (Gdx.app.getType() != ApplicationType.Desktop) return; // Camera Controls (move) float camMoveSpeed = 5 * deltaTime; float camMoveSpeedAccelerationFactor = 5; if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)) camMoveSpeed *= camMoveSpeedAccelerationFactor; if (Gdx.input.isKeyPressed(Keys.LEFT)) moveCamera(-camMoveSpeed, 0); if (Gdx.input.isKeyPressed(Keys.RIGHT)) moveCamera(camMoveSpeed, 0); if (Gdx.input.isKeyPressed(Keys.UP)) moveCamera(0, camMoveSpeed); if (Gdx.input.isKeyPressed(Keys.DOWN)) moveCamera(0, -camMoveSpeed); if (Gdx.input.isKeyPressed(Keys.BACKSPACE)) cameraHelper.setPosition(0, 0); // Camera Controls (zoom) float camZoomSpeed = 1 * deltaTime; float camZoomSpeedAccelerationFactor = 5; if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)) camZoomSpeed *= camZoomSpeedAccelerationFactor; if (Gdx.input.isKeyPressed(Keys.COMMA)) cameraHelper.addZoom(camZoomSpeed); if (Gdx.input.isKeyPressed(Keys.PERIOD)) cameraHelper.addZoom(-camZoomSpeed); if (Gdx.input.isKeyPressed(Keys.SLASH)) cameraHelper.setZoom(1); } private void moveCamera(float x, float y) { x += cameraHelper.getPosition().x; y += cameraHelper.getPosition().y; cameraHelper.setPosition(x, y); } @Override public boolean keyUp(int keycode) { if (keycode == Keys.R) { init(); Gdx.app.debug(TAG, "Game World Resetted!"); } return false; } public void init() { Gdx.input.setInputProcessor(this); cameraHelper = new CameraHelper(); lives = Constants.LIVES_START; initLevel(); } private Pixmap createProceduralPixmap(int width, int height) { Pixmap pixmap = new Pixmap(width, height, Format.RGBA8888); // Fill square with red color at 50% opacity pixmap.setColor(1, 0, 0, 0.5f); pixmap.fill(); // Draw a yellow-colored X shape on square pixmap.setColor(1, 1, 0, 1); pixmap.drawLine(0, 0, width, height); pixmap.drawLine(width, 0, 0, height); // Draw a cyan-colored border around square pixmap.setColor(0, 1, 1, 1); pixmap.drawRectangle(0, 0, width, height); return pixmap; } public void update(float deltaTime) { handleDebugInput(deltaTime); cameraHelper.update(deltaTime); } }
修改CameraHelper:(主要是将target的类型由Sprite改为AbstractGameObject)
package com.packtpub.libgdx.canyonbunny.util; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.packtpub.libgdx.canyonbunny.game.objects.AbstractGameObject; public class CameraHelper { private static final String TAG = CameraHelper.class.getName(); private final float MAX_ZOOM_IN = 0.25f; private final float MAX_ZOOM_OUT = 10.0f; private Vector2 position; private float zoom; private AbstractGameObject target; public CameraHelper() { position = new Vector2(); zoom = 1.0f; } public void update(float deltaTime) { if (!hasTarget()) return; position.x = target.position.x + target.origin.x; position.y = target.position.y + target.origin.y; } public void setPosition(float x, float y) { this.position.set(x, y); } public Vector2 getPosition() { return position; } public void addZoom(float amount) { setZoom(zoom + amount); } public void setZoom(float zoom) { this.zoom = MathUtils.clamp(zoom, MAX_ZOOM_IN, MAX_ZOOM_OUT); } public float getZoom() { return zoom; } public void setTarget(AbstractGameObject target) { this.target = target; } public AbstractGameObject getTarget() { return target; } public boolean hasTarget() { return target != null; } public boolean hasTarget(AbstractGameObject target) { return hasTarget() && this.target.equals(target); } public void applyTo(OrthographicCamera camera) { camera.position.x = position.x; camera.position.y = position.y; camera.zoom = zoom; camera.update(); } }
修改WorldRender的render():
public void render(){ renderWorld(batch); } private void renderWorld (SpriteBatch batch) { worldController.cameraHelper.applyTo(camera); batch.setProjectionMatrix(camera.combined); batch.begin(); worldController.level.render(batch); batch.end(); }
实现GUI:
Libgdx提供了默认的bitmap字体文件,arial-15.fnt和arial-15.png。用的时候可以把它们copy到images下。
我们把要用的字体(内部类)加到Assets中:
public class AssetFonts {public final BitmapFont defaultSmall; public final BitmapFont defaultNormal; public final BitmapFont defaultBig; public AssetFonts() { // create three fonts using Libgdx's 15px bitmap font defaultSmall = new BitmapFont( Gdx.files.internal("images/arial-15.fnt"), true); defaultNormal = new BitmapFont( Gdx.files.internal("images/arial-15.fnt"), true); defaultBig = new BitmapFont( Gdx.files.internal("images/arial-15.fnt"), true); // set font sizes defaultSmall.setScale(0.75f); defaultNormal.setScale(1.0f); defaultBig.setScale(2.0f); // enable linear texture filtering for smooth fonts defaultSmall.getRegion().getTexture() .setFilter(TextureFilter.Linear, TextureFilter.Linear); defaultNormal.getRegion().getTexture() .setFilter(TextureFilter.Linear, TextureFilter.Linear); defaultBig.getRegion().getTexture() .setFilter(TextureFilter.Linear, TextureFilter.Linear); } }
在init里加上字体的初始化:fonts = new AssetFonts();
在dispose里释放:fonts.defaultSmall.dispose();fonts.defaultNormal.dispose();fonts.defaultBig.dispose();
准备就绪了,我们需要先构想我们将要做的GUI图:(金币分数,兔子的额外性命,FPS)
接下来,我们在WorldRenderer中增加下面的代码:
private OrthographicCamera cameraGUI; private void init() { batch = new SpriteBatch(); camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT); camera.position.set(0, 0, 0); camera.update(); cameraGUI = new OrthographicCamera(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT); cameraGUI.position.set(0, 0, 0); cameraGUI.setToOrtho(true); // flip y-axis cameraGUI.update(); } public void resize(int width, int height) { camera.viewportWidth = (Constants.VIEWPORT_HEIGHT / height) * width; camera.update(); cameraGUI.viewportHeight = Constants.VIEWPORT_GUI_HEIGHT; cameraGUI.viewportWidth = (Constants.VIEWPORT_GUI_HEIGHT/ (float)height) * (float)width; cameraGUI.position.set(cameraGUI.viewportWidth / 2, cameraGUI.viewportHeight / 2, 0); cameraGUI.update(); }
第二个摄像机是专门用来做GUI投影渲染的。下面是每个GUI元素的具体实现方法:
private void renderGuiScore(SpriteBatch batch) { float x = -15; float y = -15; batch.draw(Assets.instance.goldCoin.goldCoin, x, y, 50, 50, 100, 100, 0.35f, -0.35f, 0); Assets.instance.fonts.defaultBig.draw(batch, "" + worldController.score, x + 75, y + 37); } private void renderGuiExtraLive(SpriteBatch batch) { float x = cameraGUI.viewportWidth - 50 - Constants.LIVES_START * 50; float y = -15; for (int i = 0; i < Constants.LIVES_START; i++) { if (worldController.lives <= i) batch.setColor(0.5f, 0.5f, 0.5f, 0.5f); batch.draw(Assets.instance.bunny.head, x + i * 50, y, 50, 50, 120, 100, 0.35f, -0.35f, 0); batch.setColor(1, 1, 1, 1); } } private void renderGuiFpsCounter(SpriteBatch batch) { float x = cameraGUI.viewportWidth - 55; float y = cameraGUI.viewportHeight - 15; int fps = Gdx.graphics.getFramesPerSecond(); BitmapFont fpsFont = Assets.instance.fonts.defaultNormal; if (fps >= 45) { // 45 or more FPS show up in green fpsFont.setColor(0, 1, 0, 1); } else if (fps >= 30) { // 30 or more FPS show up in yellow fpsFont.setColor(1, 1, 0, 1); } else { // less than 30 FPS show up in red fpsFont.setColor(1, 0, 0, 1); } fpsFont.draw(batch, "FPS: " + fps, x, y); fpsFont.setColor(1, 1, 1, 1); // white }
整合到WorldRenderer:
public void render() {renderWorld(batch); renderGui(batch); } private void renderGui(SpriteBatch batch) { batch.setProjectionMatrix(cameraGUI.combined); batch.begin(); // draw collected gold coins icon + text // (anchored to top left edge) renderGuiScore(batch); // draw extra lives icon + text (anchored to top right edge) renderGuiExtraLive(batch); // draw FPS text (anchored to bottom right edge) renderGuiFpsCounter(batch); batch.end(); }
下一章我们继续完成剩下的部分:
比如增加主角(兔子头),关卡道具(羽毛,金币),控制主角移动,基本的碰撞检测(几乎所有的游戏都有的)等等。
文章浏览阅读3.4k次,点赞8次,收藏42次。一、什么是内部类?or 内部类的概念内部类是定义在另一个类中的类;下面类TestB是类TestA的内部类。即内部类对象引用了实例化该内部对象的外围类对象。public class TestA{ class TestB {}}二、 为什么需要内部类?or 内部类有什么作用?1、 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。2、内部类可以对同一个包中的其他类隐藏起来。3、 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。三、 内部类的分类成员内部_成员内部类和局部内部类的区别
文章浏览阅读118次。分布式系统要求拆分分布式思想的实质搭配要求分布式系统要求按照某些特定的规则将项目进行拆分。如果将一个项目的所有模板功能都写到一起,当某个模块出现问题时将直接导致整个服务器出现问题。拆分按照业务拆分为不同的服务器,有效的降低系统架构的耦合性在业务拆分的基础上可按照代码层级进行拆分(view、controller、service、pojo)分布式思想的实质分布式思想的实质是为了系统的..._分布式系统运维工具
文章浏览阅读174次。1.数据源准备2.数据处理step1:数据表处理应用函数:①VLOOKUP函数; ② CONCATENATE函数终表:step2:数据透视表统计分析(1) 透视表汇总不同渠道用户数, 金额(2)透视表汇总不同日期购买用户数,金额(3)透视表汇总不同用户购买订单数,金额step3:讲第二步结果可视化, 比如, 柱形图(1)不同渠道用户数, 金额(2)不同日期..._exce l趋势分析数据量
文章浏览阅读3.3k次。堡垒机可以为企业实现服务器、网络设备、数据库、安全设备等的集中管控和安全可靠运行,帮助IT运维人员提高工作效率。通俗来说,就是用来控制哪些人可以登录哪些资产(事先防范和事中控制),以及录像记录登录资产后做了什么事情(事后溯源)。由于堡垒机内部保存着企业所有的设备资产和权限关系,是企业内部信息安全的重要一环。但目前出现的以下问题产生了很大安全隐患:密码设置过于简单,容易被暴力破解;为方便记忆,设置统一的密码,一旦单点被破,极易引发全面危机。在单一的静态密码验证机制下,登录密码是堡垒机安全的唯一_horizon宁盾双因素配置
文章浏览阅读7.7k次,点赞4次,收藏16次。Chrome作为一款挺不错的浏览器,其有着诸多的优良特性,并且支持跨平台。其支持(Windows、Linux、Mac OS X、BSD、Android),在绝大多数情况下,其的安装都很简单,但有时会由于网络原因,无法安装,所以在这里总结下Chrome的安装。Windows下的安装:在线安装:离线安装:Linux下的安装:在线安装:离线安装:..._chrome linux debian离线安装依赖
文章浏览阅读153次。中国发达城市榜单每天都在刷新,但无非是北上广轮流坐庄。北京拥有最顶尖的文化资源,上海是“摩登”的国际化大都市,广州是活力四射的千年商都。GDP和发展潜力是衡量城市的数字指...
文章浏览阅读3.3k次。前言spark在java使用比较少,多是scala的用法,我这里介绍一下我在项目中使用的代码配置详细算法的使用请点击我主页列表查看版本jar版本说明spark3.0.1scala2.12这个版本注意和spark版本对应,只是为了引jar包springboot版本2.3.2.RELEASEmaven<!-- spark --> <dependency> <gro_使用java调用spark注册进去的程序
文章浏览阅读4.8k次。汽车零部件开发工具巨头V公司全套bootloader中UDS协议栈源代码,自己完成底层外设驱动开发后,集成即可使用,代码精简高效,大厂出品有量产保证。:139800617636213023darcy169_uds协议栈 源代码
文章浏览阅读4.6k次,点赞20次,收藏148次。AUTOSAR基础篇之OS(下)前言首先,请问大家几个小小的问题,你清楚:你知道多核OS在什么场景下使用吗?多核系统OS又是如何协同启动或者关闭的呢?AUTOSAR OS存在哪些功能安全等方面的要求呢?多核OS之间的启动关闭与单核相比又存在哪些异同呢?。。。。。。今天,我们来一起探索并回答这些问题。为了便于大家理解,以下是本文的主题大纲:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JCXrdI0k-1636287756923)(https://gite_autosar 定义了 5 种多核支持类型
文章浏览阅读2.2k次,点赞6次,收藏14次。原因:自己写的头文件没有被加入到方案的包含目录中去,无法被检索到,也就无法打开。将自己写的头文件都放入header files。然后在VS界面上,右键方案名,点击属性。将自己头文件夹的目录添加进去。_vs2013打不开自己定义的头文件
文章浏览阅读3.3w次,点赞80次,收藏342次。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。此时,可以将系统中所有用户的 Session 数据全部保存到 Redis 中,用户在提交新的请求后,系统先从Redis 中查找相应的Session 数据,如果存在,则再进行相关操作,否则跳转到登录页面。当数据量很大时,count 的数量的指定可能会不起作用,Redis 会自动调整每次的遍历数目。_redis命令
文章浏览阅读449次,点赞3次,收藏3次。URP的设计目标是在保持高性能的同时,提供更多的渲染功能和自定义选项。与普通项目相比,会多出Presets文件夹,里面包含着一些设置,包括本色,声音,法线,贴图等设置。全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,主光源和附加光源在一次Pass中可以一起着色。URP:全局只有主光源和附加光源,主光源只支持平行光,附加光源数量有限制,一次Pass可以计算多个光源。可编程渲染管线:渲染策略是可以供程序员定制的,可以定制的有:光照计算和光源,深度测试,摄像机光照烘焙,后期处理策略等等。_urp渲染管线