They way we managed our assets until now is not efficient enough. Can you image what could happen if we had to load 1 huge texture atlas file for the animation of our enemies, every time a new enemy spawns? And this is only one simple example. Spikes, lags, screen freezes and even crashes could be the result of a bad management of a game's assets.
To solve this problem libGDX has a very convinient AssetManager class.
" And when is the best time to load our assets? "
Well, that is for you to decide but i always tend to do that on the splash screen. No one cares for the logo of our game, gaming company or whatever we present there. The users most probably are tapping with the speed of light the screen because they want to move on and play the actual game.
LibGDX's AssetManager
The way the AssetManager works is simple. We just have to setup an AssetManager instance, load the graphics and then when we don't need them anymore unload them. The only annoying thing, is that we have to pass the same AssetManager instance to all our classes, otherwise it's not possible to manage our assets. We also can't make the AssetManager static as mentioned in the LibGDX documentation because this could cause some issues.
To begin with, create a package with the name " data " and inside create a class and name it
" AssetMan ".
" AssetMan ".
package com.getest.game.data;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.SkinLoader;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
public class AssetMan {
private AssetManager manager;
public AssetMan() {
manager = new AssetManager();
}
public void loadAllAssets() {
SkinLoader.SkinParameter menu = new SkinLoader.SkinParameter("skins/mainMenuPack.atlas");
SkinLoader.SkinParameter buttons = new SkinLoader.SkinParameter("skins/inGameButtons.atlas");
manager.load("inGame/enemies.atlas", TextureAtlas.class);
manager.load("inGame/hero.atlas", TextureAtlas.class);
manager.load("mainMenu/mainMenu.png", Texture.class);
manager.load("inGame/backGround.png", Texture.class);
manager.load("skins/play.json", Skin.class, menu);
manager.load("skins/jumpButton.json", Skin.class, buttons);
manager.load("skins/slideButton.json", Skin.class, buttons);
manager.load("skins/rightButton.json", Skin.class, buttons);
manager.load("skins/leftButton.json", Skin.class, buttons);
}
public void unloadMenuAssets() {
manager.unload("skins/play.json");
manager.unload("mainMenu/mainMenu.png");
}
public TextureAtlas getEnemies(){
return manager.get("inGame/enemies.atlas");
}
public TextureAtlas getHero(){
return manager.get("inGame/hero.atlas");
}
public Texture getMainMenu(){
return manager.get("mainMenu/mainMenu.png");
}
public Texture getBackGround(){
return manager.get("inGame/backGround.png");
}
public Skin getPlay(){
return manager.get("skins/play.json");
}
public Skin getJump(){
return manager.get("skins/jumpButton.json");
}
public Skin getRight(){
return manager.get("skins/rightButton.json");
}
public Skin getLeft(){
return manager.get("skins/leftButton.json");
}
public Skin getSlide(){
return manager.get("skins/slideButton.json");
}
public boolean update() {
return manager.update();
}
}
The first function will load all the assets, the backgrounds (textures), the buttons (skins) and the packed frames for our animations (texture atlases). Note that we don't load the background of our splash screen because this screen is the first that will create our asset manager instance and load our assets, thus we will load and dispose the background the usual way (it is not a problem at all if we only do that for our splash screen).
Then we created the function that unloads all the main menu assets, because when we move on the game screen we won't be in any need of them. We also need a way to actually get those loaded assets, so we created the getters for all of them. The update() function needs to be called for the loading process to actually begin and it will return true when the loading is completed.
The " SplashScreen " now becomes;
package com.getest.game.screens;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.data.AssetMan;
public class SplashScreen implements Screen {
private Texture splashtexture;
private Image splashimage;
private Stage splashstage;
private float WIDTH,HEIGHT;
private AssetMan manager;
private boolean startLoading = false;
public SplashScreen() {
manager = new AssetMan();
}
@Override
public void show() {
WIDTH = 1280;
HEIGHT = 720;
splashtexture = new Texture(Gdx.files.internal("splashScreen/splashScreen.png")); splashtexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
splashimage = new Image(splashtexture);
splashimage.setSize(1280,720);
splashstage = new Stage(new FitViewport(WIDTH,HEIGHT, new AndroidCamera(WIDTH,HEIGHT))); splashstage.addActor(splashimage);
splashimage.addAction(Actions.sequence(Actions.alpha(0.0F), Actions.fadeIn(1.25F),Actions.delay(2F), Actions.run(new Runnable() {
@Override
public void run() {
manager.loadAllAssets();
startLoading = true;
}
})));
}
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
splashstage.act();
splashstage.draw();
if(manager.update() && startLoading){
((Game)Gdx.app.getApplicationListener()).setScreen(new MainMenuScreen(manager));
}
}
// Rest of the code
@Override
public void dispose() {
splashtexture.dispose();
splashstage.dispose();
}
}
As you can see, we created the first instance of our AssetMan class and then after the fade in of our gamengineering logo, we call the loadAllAssets() function that loads our assets. Then, in the render function we call the update() function and when it becomes true, we move on the main menu screen. If there are no assets to be loaded then the update() function also returns true, thus, the purpose of the startLoading boolean is to prevent the change of the screen before the fade in of the logo and the actual beginning of the assets loading.
As you can probably imagine, we have to modify every single class that will use the asset manager. The " MainMenuScreen " class now becomes;
package com.getest.game.screens;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.data.AssetMan;
public class MainMenuScreen implements Screen {
private Stage mainmenuStage;
private Skin mainmenuSkinPlay;
private ImageButton mainmenuimagebuttonPlay;
private Table mainmenuTablePlay;
private Texture mainmenuTexture;
private Image mainmenuImage;
private float WIDTH,HEIGHT;
private AssetMan manager;
public MainMenuScreen(AssetMan manager) {
this.manager = manager;
}
@Override
public void show() {
WIDTH = 1280;
HEIGHT = 720;
mainmenuTexture = manager.getMainMenu();
mainmenuTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
mainmenuImage = new Image(mainmenuTexture);
mainmenuImage.setSize(1280,720);
mainmenuStage = new Stage(new FitViewport(WIDTH,HEIGHT, new AndroidCamera(WIDTH,HEIGHT)));
mainmenuTablePlay = new Table();
mainmenuSkinPlay = manager.getPlay();
mainmenuimagebuttonPlay = new ImageButton(mainmenuSkinPlay);
mainmenuTablePlay.bottom().add(mainmenuimagebuttonPlay).size( 152F, 164F).padBottom(20F);
mainmenuStage.addActor(mainmenuImage);
mainmenuStage.addActor(mainmenuTablePlay);
Gdx.input.setInputProcessor(mainmenuStage);
mainmenuTablePlay.addAction(Actions.sequence(Actions.moveBy(0.0F, -250F), Actions.delay(1.0F), Actions.moveBy(0.0F, 250F, 1.0F, Interpolation.swing)));
mainmenuImage.addAction(Actions.sequence(Actions.alpha(0.0F), Actions.fadeIn(1.0F)));
mainmenuimagebuttonPlay.addListener(new ClickListener() {
public void clicked(InputEvent event, float x, float y) {
((Game)Gdx.app.getApplicationListener()).setScreen(new GameScreen(manager));
}
});
}
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0.0F, 0.0F, 0.0F, 0.0F);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
mainmenuStage.act();
mainmenuStage.draw();
}
// Rest of the code
@Override
public void dispose() {
manager.unloadMenuAssets();
mainmenuStage.dispose();
}
}
Nothing special here. We just used the loaded background texture and the loaded button, in the dipose() function we called the function that unloads the menu assets and we changed the way we call the game screen (because as i told you, we need to pass the same AssetMan instance to every class).
The " GameScreen " now changes to;
package com.getest.game.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.getest.game.data.AssetMan;
import com.getest.game.stages.GameStage;
public class GameScreen implements Screen {
private GameStage gameStage;
private AssetMan manager;
public GameScreen(AssetMan manager) {
this.manager = manager;
}
@Override
public void show() {
gameStage = new GameStage(manager);
}
// Rest of the code
}
You probably get the point, i am obliged to show you every change though. Go to the
" GameStage " and modify it to use the loaded background and the loaded in game buttons.
package com.getest.game.stages;
//imports ...
public class GameStage extends Stage implements ContactListener{
private float accumulator, TIME_STEP, alpha;
private Box2DDebugRenderer renderer;
private WorldMisc wrl;
private World world;
private GroundActor ground;
private LeftWallActor leftWall;
private RightWallActor rightWall;
private HeroActor hero;
private Boolean right = false, left = false;
private Skin leftButtonSkin, rightButtonSkin, jumpButtonSkin, slideButtonSkin;
private ImageButton leftButton, rightButton, jumpButton, slideButton;
private Texture backGroundTexture;
private Image backGroundImage;
private Timer timerEnemy = new Timer();
private AssetMan manager;
public GameStage(AssetMan manager){
super(new ExtendViewport(16f, 9f,
new AndroidCamera(16f, 9f)));
accumulator = 0.0F;
TIME_STEP = 1/300F; // Recomended by libgdx
this.manager = manager;
setupWorld();
}
private void setupWorld() {
wrl = new WorldMisc();
world = wrl.createWorld();
renderer = new Box2DDebugRenderer();
Gdx.input.setInputProcessor(this);
world.setContactListener(this);
setupBackGround();
setupGround();
setupLeftWall();
setupRightWall();
setupHero();
setupButtons();
createEnemy();
}
private void setupBackGround(){
backGroundTexture = manager.getBackGround();
backGroundTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
backGroundImage = new Image(backGroundTexture);
backGroundImage.setSize(19.5F, 12F);
backGroundImage.setPosition(-1.75F,0);
addActor(backGroundImage);
}
// Rest of the code
private void setupHero() {
hero = new HeroActor(wrl.createHero(world), manager);
addActor(hero);
}
private void setupButtons() {
leftButtonSkin = manager.getLeft();
rightButtonSkin = manager.getRight();
jumpButtonSkin = manager.getJump();
slideButtonSkin = manager.getSlide();
leftButton = new ImageButton(leftButtonSkin);
rightButton = new ImageButton(rightButtonSkin);
jumpButton = new ImageButton(jumpButtonSkin);
slideButton = new ImageButton(slideButtonSkin);
leftButton.setSize(2.35F, 5F);
rightButton.setSize(2.35F, 5F);
jumpButton.setSize(2.35F, 5F);
slideButton.setSize(2.35F, 5F);
leftButton.setPosition(1.25F, -0.5F);
rightButton.setPosition(4.0F, -0.5F);
jumpButton.setPosition(12.75F, -0.5F);
slideButton.setPosition(10F, -0.5F);
addActor(leftButton);
addActor(rightButton);
addActor(jumpButton);
addActor(slideButton);
leftButton.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
left = true;
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
left = false;
}
});
rightButton.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
right = true;
}
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
right = false;
}
});
jumpButton.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
hero.jump();
}
});
slideButton.addListener(new ActorGestureListener() {
public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
hero.dodge();
}
}); }
private void createEnemy() {
Timer.Task task = new Timer.Task() {
public void run() {
EnemyActor enemy = new EnemyActor(wrl.createEnemy(world), manager);
addActor(enemy);
}
};
timerEnemy.scheduleTask(task, 1.4F, 4);
}
// Rest of the code
}
I am presenting you a big portion of the previous code because we also change the way we call some functions. Just like we did with the screens, because we have to pass the same AssetMan instance, we do with the hero and enemy actors as well, because they need to use the loaded texture atlases in order to create the animations.
Finally, the " EnemyActor " and " HeroActor " classes become;
package com.getest.game.actors;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.physics.box2d.Body;
mport com.getest.game.box2d.EnemyUserData;
import com.getest.game.data.AssetMan;
public class EnemyActor extends GameActors {
private Animation<TextureRegion> animation;
private TextureRegion[] runningFrames;
private TextureAtlas textureAtlas;
private float stateTimeGroundRight,stateTimeGroundLeft,stateTimeAirLeft,stateTimeAirRight;
private AssetMan manager;
public EnemyActor(Body body, AssetMan manager) {
super(body);
this.manager = manager;
textureAtlas = manager.getEnemies();
runningFrames = new TextureRegion[getUserData().getTextureRegions().length];
for (int i = 0; i < getUserData().getTextureRegions().length; i++) {
String path = getUserData().getTextureRegions()[i];
runningFrames[i] = textureAtlas.findRegion(path);
}
animation = new Animation<TextureRegion>(0.03125f, runningFrames);
stateTimeGroundRight = 0f;
stateTimeGroundLeft = 0f;
stateTimeAirLeft = 0f;
stateTimeAirRight = 0f;
}
// Rest of the code
}
and
package com.getest.game.actors;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.utils.Timer;
import com.getest.game.box2d.HeroUserData;
import com.getest.game.data.AssetMan;
public class HeroActor extends GameActors {
private Vector2 velocityEachTime;
private Vector2 movingLinearImpulse;
private float velocityX;
private float finalVel;
private float heroPositionY, heroPositionX;
private boolean jumping = false;
private boolean dodging = false;
private boolean land = true;
private boolean runningRight = false, runningLeft = false;
private boolean right = true, left = false;
private Vector2 heroPosition;
private Timer timer1 = new Timer();
private float stateTimeRunningRight, stateTimeRunningLeft;
private String SLIDE, JUMP, IDLE;
private String [] RUNNING;
private TextureAtlas heroMovement;
private TextureRegion slideFrame, jumpFrame, idleFrame;
private Animation<TextureRegion> runningAnimation;
private AssetMan manager;
public HeroActor(Body body, AssetMan manager) {
super(body);
this.manager = manager;
RUNNING = new String[] {"1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32"};
SLIDE = "8";
JUMP = "8";
IDLE = "1";
heroMovement = manager.getHero();
TextureRegion[] runningFrames = new TextureRegion[RUNNING.length];
for (int i = 0; i < RUNNING.length ; i++){
String pathRunning = RUNNING[i];
runningFrames[i] = heroMovement.findRegion(pathRunning);
}
runningAnimation = new Animation<TextureRegion>(0.03125f,runningFrames);
slideFrame = heroMovement.findRegion(SLIDE);
idleFrame = heroMovement.findRegion(IDLE);
jumpFrame = heroMovement.findRegion(JUMP);
stateTimeRunningRight = 0f;
stateTimeRunningLeft = 0f;
}
// Rest of the code
}
That was it! Run the game and notice no difference at all. It is okay though, we know for a fact that we did a great job managing our assets. We now have no fear of lags and spikes caused by a bad asset management.
Comments
Post a Comment