Hey,
I’ve been having a bit of trouble figuring out how to use a camera to move around my map and render the player and other entities onto the map with working zooming.
If someone has some tips, suggestions, or sees a way to do this properly with my current code that would be appreciated.
Rendering the map:
package valkryst.area.map;
import java.awt.Graphics;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import valkryst.area.Area;
import valkryst.area.map.tile.GrassTile;
import valkryst.area.map.tile.VoidTile;
import valkryst.core.Camera;
/**
* This class adds the map to the pixel array that will be rendered onto the screen.
*
* @author Valkryst
* --- Last Edit 24-Oct-2013
*/
public class RenderMap {
public static String[] map;
private static int currentMapID = -1;
private static VoidTile voidTile = new VoidTile();
private static GrassTile grassTile = new GrassTile();
/**
* Renders the map to the canvas.
* @param g The graphics object to use to render things to the canvas with.
* @param area The area that the player is currently in.
* @param p The player.
* @param width The width of the canvas.
* @param height The height of the canvas.
*/
public void render(final Graphics g, final Area area, final Camera c, final int width, final int height, final int scaleMultiplier) {
int areaID = area.getMapID();
if(areaID != currentMapID || currentMapID == -1) {
loadMapFromFile(areaID);
currentMapID = areaID;
}
int xOffset = c.getOriginX() >> 5; // The x-axis tile coordinate of where the cameras origin is currently.
int yOffset = c.getOriginY() >> 5; // The y-axis tile coordinate of where the cameras origin is currently.
int numberOfXTiles = (width >> 5); // Takes whatever the width of the canvas is, divides it by 32 and then that's then number of XTiles to draw.
int numberOfYTiles = (height >> 5); // Takes whatever the height of the canvas is, divides it by 32 and then that's then number of YTiles to draw.
if(width > 0 && height > 0) { // If there is no room on the screen to render the map then don't bother rendering it.
for(int y = 0 + yOffset; y < yOffset + numberOfYTiles + 2; y++) {
for(int x = 0 + xOffset; x < xOffset + numberOfXTiles + 2; x++) {
try {
switch(map[y].charAt(x)) {
case 'A': {
g.drawImage(grassTile.getTileImage(), (x - xOffset) * scaleMultiplier, (y - yOffset) * scaleMultiplier, scaleMultiplier, scaleMultiplier, null);
break;
}
default: {
g.drawImage(voidTile.getTileImage(), (x - xOffset) * scaleMultiplier, (y - yOffset) * scaleMultiplier, scaleMultiplier, scaleMultiplier, null);
}
}
} catch(Exception e) {
continue;
}
}
}
}
}
public void loadMapFromFile(final int mapID) {
List<String> loadedMap = new ArrayList<String>();
InputStream is = this.getClass().getClassLoader().getResourceAsStream("Areas/Area" + mapID + ".txt");
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = bufferedReader.readLine()) != null) {
loadedMap.add(line);
}
bufferedReader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
map = loadedMap.toArray(new String[loadedMap.size()]);
System.out.println("Sucessfully finished loading Areas/Area" + mapID + ".txt");
}
}
The Camera:
package valkryst.core;
import valkryst.area.Area;
import valkryst.core.graphics.Frame;
public class Camera {
private int mapSizeInPixels;
/** The origin for the are that the camera is currently viewing. */
private int originX = 0, originY = 0;
/** The start and end bounds of the map. */
private int start = 0, end;
public Camera(final Area currentAreaIn) {
mapSizeInPixels = currentAreaIn.getMapSize() * (32 * Frame.scale);
end = mapSizeInPixels;
}
/**
* Moves the camera's origin.
* @param x The value to add to the originX variable.
* @param y The value to add to the originY variable.
*/
public void moveOrigin(int x, int y) {
originX += x;
originY += y;
checkBounds();
}
public void checkBounds() {
// Checks if the originX is below 0, if it is then set it to 0.
if(originX < start) {
originX = start;
}
// Checks if the originY is below 0, if it is then set it to 0.
if(originY < start) {
originY = start;
}
}
/**
* Sets the bounds of the map.
* @param currentArea The area to create bounds for.
*/
public void changeBounds(final Area currentAreaIn) {
mapSizeInPixels = currentAreaIn.getMapSize() * (32 * Frame.scale);
end = mapSizeInPixels;
}
///////////////////////////////////////// Get methods here: ////////////////////////////////////////////////////
/**
* Returns the originX variable of the camera.
* @return Returns the originX variable of the camera.
*/
public int getOriginX() {
return originX;
}
/**
* Returns the originY variable of the camera.
* @return Returns the originY variable of the camera.
*/
public int getOriginY() {
return originY;
}
}
The Player:
package valkryst.entity;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import valkryst.area.Area;
import valkryst.core.Camera;
import valkryst.core.graphics.Frame;
import valkryst.enums.BattleDecision;
import valkryst.enums.CombatType;
/**
* This class defines a player object.
*
* @author Valkryst
* --- Last Edit 24-Oct-2013
*/
public class Player extends Entity {
private BattleDecision battleDecision = null;
/**
* The constructor for the player object.
* @param nameIn The name of the player being constructed.
* @param playerLevelIn The level of the player being constructed.
* @param playerMaxLevelIn The maximum level of the player being constructed.
* @param combatTypeIn The combat type of the player being constructed.
*/
public Player(final String nameIn, final int playerLevelIn, final int playerMaxLevelIn, final CombatType combatTypeIn) {
setName(nameIn);
setUsesArmor(true);
setUsesWeapon(true);
setLevel(playerLevelIn);
setMaxLevel(playerMaxLevelIn);
setExperience(0);
double health = (playerLevelIn * 25.0);
setHealth(health);
setMaxHealth(health);
switch (combatTypeIn) {
case MELEE:
setEnergy(100.0);
setMaxEnergy(100.0);
setUsesEnergy(true);
break;
case RANGED:
setEnergy(100.0);
setMaxEnergy(100.0);
setUsesEnergy(true);
break;
case CASTER:
setMana(100.0);
setMaxMana(100.0);
setUsesMana(true);
break;
default: {
final Logger LOGGER = LoggerFactory.getLogger(Player.class);
LOGGER.info("Unsupported type of CombatType: " + combatTypeIn);
}
}
setBaseDamage(3.5 * getLevel());
setArmorPoints(0.0);
setup(true);
}
/**
* Updates the player in preparation to be rendered.
*/
public void updateLogic(final Area currentArea, final Camera camera) {
// Checks whether the player entity is visible on the screen or not.
if(xCoord + sprite.getSpriteImage(0).getWidth(null) < camera.getOriginX() && yCoord + sprite.getSpriteImage(0).getHeight(null) < camera.getOriginY()) {
isVisible = false;
} else if (!isVisible){
isVisible = true;
}
// Moves + or - one pixel and + or - 1/32 of a tile for every pixel moved.
if(Frame.KEYBOARD_INPUT.isKeyPressed(KeyEvent.VK_UP)) {
move(0, -1, 0, -0.03125);
camera.moveOrigin(0, -1);
}
if(Frame.KEYBOARD_INPUT.isKeyPressed(KeyEvent.VK_DOWN)) {
move(0, 1, 0, 0.03125);
camera.moveOrigin(0, 1);
}
if(Frame.KEYBOARD_INPUT.isKeyPressed(KeyEvent.VK_LEFT)) {
move(-1, 0, -0.03125, 0);
camera.moveOrigin(-1, 0);
}
if(Frame.KEYBOARD_INPUT.isKeyPressed(KeyEvent.VK_RIGHT)) {
move(1, 0, 0.03125, 0);
camera.moveOrigin(1, 0);
}
// Checks if the player entity is off of bounds and if it is then move it back onto the map.
if(xCoord < 0 || tileX < 0) {
xCoord = 0;
tileX = 0;
}
if(yCoord < 0 || tileY < 0) {
yCoord = 0;
tileY = 0;
}
int mapSize = currentArea.getMapSize();
int scale = (32 * Frame.scale);
int mapSizeInPixels = (mapSize * scale);
if(xCoord > mapSizeInPixels || tileX > mapSize) {
System.out.println("Test1");
xCoord = mapSizeInPixels;
tileX = mapSize-1;
}
if(yCoord > mapSizeInPixels || tileY > mapSize) {
System.out.println("Test2");
yCoord = mapSizeInPixels;
tileY = mapSize - 1;
}
}
/**
* Renders the player to the screen.
*/
public void render(final Graphics g, final int scaleMultiplier) {
if(isVisible) {
g.drawImage(getSprite().getSpriteImage(0), xCoord, yCoord, scaleMultiplier, scaleMultiplier , null);
}
}
///////////////////////////////////////// Get methods here: ////////////////////////////////////////////////////
public BattleDecision getBattleDecision() {
return battleDecision;
}
///////////////////////////////////////// Set methods here: ////////////////////////////////////////////////////
public void setMyBattleDecision(final BattleDecision b) {
battleDecision = b;
}
}
The Creature:
package valkryst.entity;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import valkryst.enums.CombatType;
import valkryst.systems.Drops;
import valkryst.systems.battle.AITactics;
/**
* This class defines a creature object.
*
* @author Valkryst
* --- Last Edit 24-Oct-2013
*/
public class Creature extends Entity {
public final AITactics TACTICS = new AITactics();
/** The id of the creature. */
final int CREATURE_ID;
/** The drop table of the creature. */
private Drops drops;
/** Whether the creature has been looted or not. If the creature has been looted then it can be removed from the area it's on. */
private boolean hasBeenLooted = false;
/**
* The constructor method for the Creature object.
* @param creatureIdIn The ID of the creature being created.
* @param nameIn The name of the creature being created.
* @param creatureLevelIn The level of the creature being created.
* @param creatureMaxLevelIn The maximum level of the creature being created.
* @param combatTypeIn The combat type of the creature being created.
*/
public Creature(final int creatureIdIn, final String nameIn, final int creatureLevelIn, final int creatureMaxLevelIn, final CombatType combatTypeIn) {
CREATURE_ID = creatureIdIn; // Set the creature's ID.
setName(nameIn);// Set the creature's name.
int level = -1;
while(level <= 0) { // Prevents the creature's level from being 0 or below.
// Generates a random integer that is within playerLevel-3 and playerLevel+3.
level = (creatureLevelIn - 3) + (int) (Math.random() * (((creatureLevelIn + 3) - (creatureLevelIn - 3)) + 1));
}
setLevel(level);
setMaxLevel(creatureMaxLevelIn);
setExperience(0);
// Figure out the creature's health.
final Random RANDOM = new Random();
double health = 0;
while(health <= 0 || health == -1) {
health = (level * 25.0) + RANDOM.nextInt((level * 10) + 1);
}
setHealth(health);
setMaxHealth(health);
// Figure out the creature's aggressiveness. REWORK LATER ON
if(level >= creatureLevelIn) {
setAggressiveness(Math.random() + (level - creatureLevelIn)/100); // Tested multiple times and has never gone to 1.0 or above. Do extensive testing if it ever does tho.
} else if(getAggressiveness() == -1) {
setAggressiveness(Math.random());
}
// Set the creature's energy/mana depending on it's combatType.
switch (combatTypeIn) {
case MELEE: {
setEnergy(100.0);
setMaxEnergy(100.0);
setUsesEnergy(true);
break;
}
case RANGED: {
setEnergy(100.0);
setMaxEnergy(100.0);
setUsesEnergy(true);
break;
}
case CASTER: {
setMana(100.0);
setMaxMana(100.0);
setUsesMana(true);
break;
}
default: {
final Logger LOGGER = LoggerFactory.getLogger(Creature.class);
LOGGER.info("Unsupported type of CombatType: " + combatTypeIn);
}
}
setBaseDamage(2.5 * level); // Set the creature's base damage.
setArmorPoints(0.0); // Set the armor points of the creature.
setup(false);
}
public void createDropTable(final int[] dropTable, final double[] dropChance) {
drops.createDropTable(dropTable, dropChance);
}
/**
* Updates the creature in preparation to be rendered.
*/
public void updateLogic() {
}
/**
* Renders the player to the screen.
*/
public void render() {
}
///////////////////////////////////////// Set methods here: ////////////////////////////////////////////////////
/**
* Sets whether the creature has been looted or not.
* @param b The variable to set the hasBeenLooted variable to.
*/
public void setHasBeenLooted(boolean b) {
hasBeenLooted = b;
}
///////////////////////////////////////// Get methods here: ////////////////////////////////////////////////////
/**
* Returns the drop variable of the creature.
* @return Returns the drop variable of the creature.
*/
public Drops getDrops() {
return drops;
}
/**
* Returns whether the creature has been looted or not.
* @return Returns whether the creature has been looted or not.
*/
public boolean getHasBeenLooted() {
return hasBeenLooted;
}
}
The Game Loop:
package valkryst.core.graphics;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import javax.swing.SwingUtilities;
import valkryst.area.Area;
import valkryst.area.map.RenderMap;
import valkryst.core.Camera;
import valkryst.entity.Player;
import valkryst.enums.CombatType;
import valkryst.spell.Cast;
/**
* @author Valkryst
* --- Last Edit 24-Oct-2013
*/
public class Screen extends Canvas implements Runnable{
private static final long serialVersionUID = 4532836895892068039L;
private Thread gameThread;
private boolean isGameRunning = false;
private int WIDTH = Frame.WIDTH, HEIGHT = Frame.HEIGHT, SCALE = Frame.scale;
private BufferStrategy BS = getBufferStrategy();
private Area currentArea = new Area(0, 10, "TestArea"); // area shouldn't be created here.
int lastFrameScale = 0, scaleMultiplier = (32 * Frame.scale);
private RenderMap renderMap = new RenderMap();
private Camera camera;
public Screen() {
setPreferredSize(new Dimension(WIDTH * SCALE, HEIGHT * SCALE));
setFocusable(true);
setVisible(true);
}
public void run() {
long lastLoopTime = System.nanoTime();
final int TARGET_FPS = 60;
final long OPTIMAL_TIME = 1000000000 / TARGET_FPS;
double delta = 0;
// Keep looping until the game ends.
while (isGameRunning) {
long now = System.nanoTime();
long updateLength = now - lastLoopTime;
lastLoopTime = now;
delta += updateLength / ((double)OPTIMAL_TIME); // Work out how long its been since the last update. This will be used to calculate how far the entities should move this loop.
//Update the game's logic and then render the screen.
while(delta >= 1) {
updateLogic(delta);
delta--;
}
render();
// we want each frame to take 10 milliseconds, to do this
// we've recorded when we started the frame. We add 10 milliseconds
// to this and then factor in the current time to give
// us our final value to wait for
// remember this is in ms, whereas our lastLoopTime etc. vars are in ns.
try {
long tempLong = (lastLoopTime-System.nanoTime() + OPTIMAL_TIME)/1000000;
if(tempLong <= 0) { continue; } // Skips the sleep()
Thread.sleep(tempLong);
} catch (InterruptedException e) {
continue;
}
}
stop();
}
public synchronized void start() {
// TEMPORARY
currentArea.addPlayerToArea(new Player("Test", 1, (short)60, CombatType.CASTER));
// END OF TEMPORARY
camera = new Camera(currentArea);
setBackground(Color.black);
isGameRunning = true;
gameThread = new Thread(this, "Display");
gameThread.start();
}
public synchronized void stop() {
try {
gameThread.join();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// When called this updates all of the game's logic.
public void updateLogic(double delta) {
Cast.setCurrentTime();
currentArea.pruneCreatures(); // Removes all creatures that have been looted from the area.
currentArea.updateLogic(camera);
}
// When called this updates the screen.
public void render() {
// If Frame.scale has changed then re-size all tile images.
if(Frame.scale != lastFrameScale) {
scaleMultiplier = (32 * Frame.scale);
}
lastFrameScale = Frame.scale;
// Forces the canvas to use triple buffering.
BS = getBufferStrategy();
if (BS == null) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createBufferStrategy(3);
}
});
return;
}
// Creates the graphics object and then clears the screen.
Graphics g = BS.getDrawGraphics();
g.clearRect(0, 0, getWidth(), getHeight());
renderMap.render(g, currentArea, camera, getWidth(), getHeight(), scaleMultiplier);
currentArea.render(g, scaleMultiplier);
g.dispose();
BS.show();
}
}
Thanks for any replies!