package me.hammerle.supersnuvi.rendering; import java.util.ArrayList; import me.hammerle.snuviengine.api.Shader; import me.hammerle.snuviengine.api.Texture; import me.hammerle.snuviengine.api.TextureRenderer; import me.hammerle.snuviengine.util.Rectangle; import me.hammerle.supersnuvi.Game; import me.hammerle.supersnuvi.entity.Entity; import me.hammerle.supersnuvi.gamelogic.Level; import me.hammerle.supersnuvi.gamelogic.LevelData; import me.hammerle.supersnuvi.tiles.Tile; import me.hammerle.supersnuvi.util.Utils; public class LevelRenderer implements IRenderer { public final static float ERROR = 1f / 65536f; public final static Texture TILES = new Texture("resources/tiles.png"); private final static Texture GUI = new Texture("resources/gui.png"); private final static TextureRenderer GUI_RENDERER = new TextureRenderer(60); private final static int MESH_SIZE = 16; private float cameraX = 0.0f; private float cameraY = 0.0f; private Level lastRenderedLevel = null; private int meshLayers = 0; private int meshWidth = 0; private int meshHeight = 0; private TextureRenderer[][][] meshes = null; @Override public void renderTick(Shader sh, float lag, Level level) { updateCamera(sh, lag, level); if(lastRenderedLevel != level) { updateMeshes(level); lastRenderedLevel = level; for(int i = 0; i < 32; i++) { level.getLight(i).markDirty(); } } Shader.setAmbientLight(level.getAmbientRed(), level.getAmbientGreen(), level.getAmbientBlue()); Shader.setLightEnabled(true); for(int i = 0; i < 1; i++) { Light l = level.getLight(i); if(l.isDirty()) { sh.setLightColor(i, l.getRed(), l.getGreen(), l.getBlue()); sh.setLightStrength(i, l.getStrength()); l.clearDirtyFlag(); } sh.setLightLocation(i, Utils.interpolate(l.getLastX(), l.getX(), lag) + cameraX, Utils.interpolate(l.getLastY(), l.getY(), lag) + cameraY); } renderTilesAndEntities(sh, lag, level); Shader.setLightEnabled(false); renderOverlay(sh, level); } private float getView(int screenSize, int levelSize, float pos) { pos -= screenSize >> 1; if(pos < 0) { return 0; } float max = levelSize * Tile.SIZE - screenSize; if(pos > max) { return max; } return pos; } private void updateCamera(Shader sh, float lag, Level level) { Entity hero = level.getHero(); int levelWidth = level.getData().getWidth(); int levelHeight = level.getData().getHeight(); int screenWidth = sh.getViewWidth(); int screenHeight = sh.getViewHeight(); float halfHeroWidth = hero.getWidth() * 0.5f; float halfHeroHeight = hero.getHeight() * 0.5f; float oldCamX = -getView(screenWidth, levelWidth, hero.getLastX() + halfHeroWidth); float oldCamY = -getView(screenHeight, levelHeight, hero.getLastY() + halfHeroHeight); float camX = -getView(screenWidth, levelWidth, hero.getX() + halfHeroWidth); float camY = -getView(screenHeight, levelHeight, hero.getY() + halfHeroHeight); cameraX = Utils.interpolate(oldCamX, camX, lag); cameraY = Utils.interpolate(oldCamY, camY, lag); sh.translateTo(cameraX, cameraY); sh.updateMatrix(); } private void updateMeshes(Level level) { if(meshes != null) { for(int l = 0; l < meshLayers; l++) { for(int mx = 0; mx < meshWidth; mx++) { for(int my = 0; my < meshHeight; my++) { meshes[l][mx][my].delete(); } } } } meshLayers = level.getData().getLayers() - 1; meshWidth = (int) Math.ceil((double) level.getData().getWidth() / MESH_SIZE); meshHeight = (int) Math.ceil((double) level.getData().getHeight() / MESH_SIZE); meshes = new TextureRenderer[meshLayers][meshWidth][meshHeight]; for(int l = 0; l < meshLayers; l++) { for(int mx = 0; mx < meshWidth; mx++) { for(int my = 0; my < meshHeight; my++) { meshes[l][mx][my] = new TextureRenderer(MESH_SIZE * MESH_SIZE * 2, false); } } } } private void drawMesh(Level level, int l, int tl, int mx, int my) { TextureRenderer tr = meshes[l][mx][my]; if(!tr.isBuilt()) { LevelData data = level.getData(); int tsx = mx * MESH_SIZE; int tsy = my * MESH_SIZE; int tex = Math.min(tsx + MESH_SIZE, data.getWidth()); int tey = Math.min(tsy + MESH_SIZE, data.getHeight()); for(int x = tsx; x < tex; x++) { for(int y = tsy; y < tey; y++) { Tile t = Game.get().getTile(data.getTile(tl, x, y)); if(t.shouldRender(x, y, level)) { float minX = x * Tile.SIZE + t.getOffsetX(); float minY = y * Tile.SIZE + t.getOffsetY(); tr.addRectangle(minX, minY, minX + t.getWidth(), minY + t.getHeight(), t.getTextureMinX(x, y, level) + ERROR, t.getTextureMinY(x, y, level) + ERROR, t.getTextureMaxX(x, y, level) - ERROR, t.getTextureMaxY(x, y, level) - ERROR); } } } tr.build(); } tr.draw(); } private void renderTilesAndEntities(Shader sh, float lag, Level level) { int startX = (int) (-cameraX / (MESH_SIZE * Tile.SIZE)); int startY = (int) (-cameraY / (MESH_SIZE * Tile.SIZE)); int endX = (int) Math.ceil((-cameraX + sh.getViewWidth()) / (MESH_SIZE * Tile.SIZE)); int endY = (int) Math.ceil((-cameraY + sh.getViewHeight()) / (MESH_SIZE * Tile.SIZE)); startX = Math.min(Math.max(startX, 0), meshWidth); startY = Math.min(Math.max(startY, 0), meshHeight); endX = Math.min(Math.max(endX, 0), meshWidth); endY = Math.min(Math.max(endY, 0), meshHeight); // clear meshes which need an update if(level.getTileUpdater().shouldUpdateAll()) { if(meshes != null) { for(int l = 0; l < meshLayers; l++) { for(int mx = 0; mx < meshWidth; mx++) { for(int my = 0; my < meshHeight; my++) { meshes[l][mx][my].clear(); } } } } } else { level.getTileUpdater().forEach((layer, x, y) -> { if(layer == level.getData().getBackgroundIndex() + 1) { // do not update changes on entity layer return; } if(layer > level.getData().getBackgroundIndex()) { layer--; } meshes[layer][x / MESH_SIZE][y / MESH_SIZE].clear(); }); } level.getTileUpdater().clear(); // background sh.setColorEnabled(false); sh.setTextureEnabled(true); sh.setBlendingEnabled(true); TILES.bind(); LevelData data = level.getData(); int fromLayer = 0; int toLayer = level.getData().getBackgroundIndex() + 1; for(int l = fromLayer; l < toLayer; l++) { for(int mx = startX; mx < endX; mx++) { for(int my = startY; my < endY; my++) { drawMesh(level, l, l, mx, my); } } } // entities level.forEachEntity(entity -> entity.renderTick(lag)); // foreground sh.setColorEnabled(false); sh.setTextureEnabled(true); sh.setBlendingEnabled(true); TILES.bind(); fromLayer = toLayer + 1; toLayer = level.getData().getLayers(); for(int l = fromLayer; l < toLayer; l++) { for(int mx = startX; mx < endX; mx++) { for(int my = startY; my < endY; my++) { drawMesh(level, l - 1, l, mx, my); } } } } public static String formatBottles(int bottles, int maxBottles) { char[] c = new char[5]; if(bottles <= 9) { c[0] = '0'; c[1] = (char) (bottles + '0'); } else if(bottles > 99) { c[0] = 'X'; c[1] = 'X'; } else { c[0] = (char) ((bottles / 10) + '0'); c[1] = (char) ((bottles % 10) + '0'); } c[2] = '/'; int currentMaxSouls = Math.max(bottles, maxBottles); if(currentMaxSouls <= 9) { c[3] = '0'; c[4] = (char) (currentMaxSouls + '0'); } else if(currentMaxSouls > 99) { c[3] = 'X'; c[4] = 'X'; } else { c[3] = (char) ((currentMaxSouls / 10) + '0'); c[4] = (char) ((currentMaxSouls % 10) + '0'); } return new String(c); } public static String formatTime(float time) { if(time == -1.0f) { return "-----"; } else if(time >= 999.9f) { return "999.9"; } return String.format("%05.1f", time); } private String[] split(String s) { ArrayList list = new ArrayList<>(); int old = 0; int index = 0; while(index < s.length()) { switch(s.charAt(index)) { case '\n': list.add(s.substring(old, index)); list.add("\n"); old = index + 1; break; case ' ': list.add(s.substring(old, index)); old = index + 1; break; } index++; } if(old < s.length()) { list.add(s.substring(old, index)); } return list.toArray(new String[list.size()]); } private void renderMessage(Shader sh, Level level) { String message = level.getMessage(); if(message != null) { float lineWidth = sh.getFontRenderer().getWidth(); String[] messageParts = split(message); int index = 0; ArrayList list = new ArrayList<>(); list.add(new StringBuilder()); float currentWidth = 0; float w = sh.getViewWidth() - 26; for(String s : messageParts) { if(s.equals("\n")) { currentWidth = w; continue; } Rectangle rec = sh.getFontRenderer().getSize(s); // + lineWidth for the space if(currentWidth + rec.getWidth() + lineWidth < w) { currentWidth += rec.getWidth(); StringBuilder sb = list.get(index); if(sb.length() == 0) { sb.append(s); } else { sb.append(" "); sb.append(s); currentWidth += lineWidth; } } else { StringBuilder sb = new StringBuilder(); list.add(sb); index++; sb.append(s); currentWidth = rec.getWidth(); } } float height = list.size() * sh.getFontRenderer().getHeight(); sh.setColorEnabled(true); sh.setTextureEnabled(false); float messageY = sh.getViewHeight() - height - 26; sh.getColorRenderer().drawRectangle(0.0f, messageY, sh.getViewWidth(), sh.getViewHeight(), 0x77000000); messageY += 13; sh.setTextureEnabled(true); for(StringBuilder sb : list) { messageY = sh.getFontRenderer().drawString(13.0f, messageY, sb.toString()); } sh.setColorEnabled(false); } } private void renderOverlay(Shader sh, Level level) { // menu rendering sh.translateTo(0.0f, 0.0f); sh.updateMatrix(); // grey background of clock and bottles float lineHeight = sh.getFontRenderer().getHeight(); float lineWidth = sh.getFontRenderer().getWidth(); sh.setColorEnabled(true); sh.setTextureEnabled(false); sh.getColorRenderer().drawRectangle(0.0f, 0.0f, (lineWidth * 6.0f) + 10.0f, (lineHeight * 2.0f + 10.0f), 0x77000000); sh.setTextureEnabled(true); float y = 5.0f; y = sh.getFontRenderer().drawString(13.0f, y, formatBottles(level.getCurrentBottles(), level.getMaxBottles())); sh.getFontRenderer().drawString(13.0f, y, formatTime(level.getTime())); sh.setColorEnabled(false); // draw messages renderMessage(sh, level); GUI.bind(); GUI_RENDERER.clear(); int scale = sh.getViewScale(); // bottles switch(scale) { case 1: GUI_RENDERER.addRectangle(6.0f, 4.0f, 12.0f, 14.0f, 0.0f, 0.046875f, 0.01171875f, 0.068359375f); break; case 2: GUI_RENDERER.addRectangle(6.0f, 4.0f, 12.0f, 14.0f, 0.01171875f, 0.046875f, 0.037109375f, 0.0859375f); break; default: GUI_RENDERER.addRectangle(6.0f, 4.0f, 12.0f, 14.0f, 0.037109375f, 0.046875f, 0.06640625f, 0.10546875f); break; } // clock switch(scale) { case 1: GUI_RENDERER.addRectangle(4.0f, y, 13.0f, y + 9.0f, 0.0f, 0.265625f, 0.017578125f, 0.283203125f); break; case 2: GUI_RENDERER.addRectangle(4.5f, y, 13.0f, y + 8.5f, 0.017578125f, 0.265625f, 0.05078125f, 0.298828125f); break; default: GUI_RENDERER.addRectangle(4.666666666f, y, 13.0f, y + 8.333333333f, 0.05078125f, 0.265625f, 0.099609375f, 0.314453125f); break; } float w = sh.getViewWidth(); // gui background GUI_RENDERER.addRectangle(w - 111.0f, 0.0f, w - 44.0f, 24.0f, 0.0f, 0.0f, 0.130859375f, 0.046875f); GUI_RENDERER.addRectangle(w - 44.0f, 0.0f, w - 18.0f, 16.0f, 0.130859375f, 0.0f, 0.181640625f, 0.03125f); GUI_RENDERER.addRectangle(w - 76.0f, 24.0f, w - 45.0f, 57.0f, 0.068359375f, 0.046875f, 0.12890625f, 0.111328125f); // health mirror int healthFrame = (int) (level.getHero().getHealth().getHealthPercent() * 7); float leftMirror = (7 - healthFrame) * 0.0625f; GUI_RENDERER.addRectangle(w - 39.0f, 8.0f, w - 7.0f, 46.0f, leftMirror, 0.15625f, leftMirror + 0.0625f, 0.23046875f); // energy float energy = level.getHero().getEnergy().getEnergyPercent(); float fullEnd = w - 109.0f + 64.0f * energy; GUI_RENDERER.addRectangle(w - 109.0f, 13.0f, fullEnd, 21.0f, 0.0f, 0.140625f, 0.125f * energy, 0.15625f); GUI_RENDERER.addRectangle(fullEnd, 13.0f, w - 45.0f, 21.0f, 0.125f * energy, 0.125f, 0.125f, 0.140625f); // gui foreground GUI_RENDERER.addRectangle(w - 49.0f, 0.0f, w, 64.0f, 0.201171875f, 0.0f, 0.296875f, 0.125f); GUI_RENDERER.addRectangle(w - 109.0f, 15.0f, w - 106.0f, 18.0f, 0.15625f, 0.03125f, 0.162109375f, 0.037109375f); GUI_RENDERER.addRectangle(w - 97.0f, 15.0f, w - 92.0f, 20.0f, 0.1796875f, 0.03125f, 0.189453125f, 0.041015625f); // health number overlay GUI_RENDERER.addRectangle(w - 30.0f, 53.0f, w - 12.0f, 62.0f, leftMirror, 0.23828125f, leftMirror + 0.03515625f, 0.255859375f); GUI_RENDERER.build(); GUI_RENDERER.draw(); // dynamic clock hand sh.setColorEnabled(true); sh.setTextureEnabled(false); switch(scale) { case 1: sh.translateTo(8.5f, y + 4.5f); break; case 2: sh.translateTo(8.75f, y + 4.25f); break; default: sh.translateTo(8.8333333333f, y + 4.16666667f); break; } sh.rotate(-level.getTime() * 72.0f); sh.updateMatrix(); sh.getColorRenderer().drawRectangle(-0.5f / scale, -0.5f / scale, 0.5f / scale, 4.0f - 0.5f * scale, 0xFF000000); } }