/******************************************************************************* * Copyright (c) 2013 Philip Collin. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * Philip Collin - initial API and implementation ******************************************************************************/ package com.lyeeedar.Roguelike3D.Game; import java.util.ArrayDeque; import com.lyeeedar.Roguelike3D.Game.Level.Tile; public class Shadow { private static final int viewRange = 20; private int startX; private int startY; public Shadow() { } // Takes a circle in the form of a center point and radius, and a function that // can tell whether a given cell is opaque. Calls the setFoV action on // every cell that is both within the radius and visible from the center. public void ComputeFieldOfViewWithShadowCasting( int x, int y, Tile[][] level) { this.startX = x; this.startY = y; for (int i = 0; i < level.length; i ++) { for (int j = 0; j < level[0].length; j++) { level[i][j].visible = false; } } for (int octant = 0; octant < 8; ++octant) { ComputeFieldOfViewInOctantZero(level, octant); } } private void ComputeFieldOfViewInOctantZero(Tile[][] level, int octant) { ArrayDeque<Column> queue = new ArrayDeque<Column>(); queue.addFirst(new Column(0, new int[]{1, 0}, new int[]{1, 1}, octant)); while (queue.size() != 0) { Column current = queue.pollLast(); if (current.getX() > viewRange) { continue; } ComputeFoVForColumnPortion( current.getX(), current.getTopVector(), current.getBottomVector(), queue, current.getOctant(), level); } } // This method has two main purposes: (1) it marks points inside the // portion that are within the radius as in the field of view, and // (2) it computes which portions of the following column are in the // field of view, and puts them on a work queue for later processing. private void ComputeFoVForColumnPortion( int x, int[] topVector, int[] bottomVector, ArrayDeque<Column> queue, int octant, Tile[][] level) { // Search for transitions from opaque to transparent or // transparent to opaque and use those to determine what // portions of the *next* column are visible from the origin. // Start at the top of the column portion and work down. int topY; if (x == 0) { topY = 0; } else { int quotient = (2 * x + 1) * topVector[1] / (2 * topVector[0]); int remainder = (2 * x + 1) * topVector[1] % (2 * topVector[0]); if (remainder > topVector[0]) topY = quotient + 1; else topY = quotient; } // Note that this can find a top cell that is actually entirely blocked by // the cell below it; consider detecting and eliminating that. int bottomY; if (x == 0) { bottomY = 0; } else { int quotient = (2 * x - 1) * bottomVector[1] / (2 * bottomVector[0]); int remainder = (2 * x - 1) * bottomVector[1] % (2 * bottomVector[0]); if (remainder >= bottomVector[0]) bottomY = quotient + 1; else bottomY = quotient; } // A more sophisticated algorithm would say that a cell is visible if there is // *any* straight line segment that passes through *any* portion of the origin cell // and any portion of the target cell, passing through only transparent cells // along the way. This is the "Permissive Field Of View" algorithm, and it // is much harder to implement. Boolean wasLastCellOpaque = null; for (int y = topY; y >= bottomY; --y) { boolean inRadius = IsInRadius(x, y); if (inRadius) { // The current cell is in the field of view. int[] temp = TranslateOctant(new int[]{x,y}, octant); if ((temp[0] < 0) || (temp[1] < 0) || (temp[0] > level.length) || temp[1] > level[0].length) { continue; } level[temp[0]][temp[1]].visible = true; if ((!level[temp[0]][temp[1]].seen) && (!(GameData.level.checkOpaque(level[temp[0]][temp[1]])))) level[temp[0]][temp[1]].seen = true; } // A cell that was too far away to be seen is effectively // an opaque cell; nothing "above" it is going to be visible // in the next column, so we might as well treat it as // an opaque cell and not scan the cells that are also too // far away in the next column. boolean currentIsOpaque = !inRadius || isOpaque(x, y, octant); if (wasLastCellOpaque != null) { if (currentIsOpaque) { // We've found a boundary from transparent to opaque. Make a note // of it and revisit it later. if (!wasLastCellOpaque.booleanValue()) { // The new bottom vector touches the upper left corner of // opaque cell that is below the transparent cell. queue.addFirst(new Column( x + 1, new int[]{x * 2 - 1, y * 2 + 1}, topVector, octant)); } } else if (wasLastCellOpaque.booleanValue()) { // We've found a boundary from opaque to transparent. Adjust the // top vector so that when we find the next boundary or do // the bottom cell, we have the right top vector. // // The new top vector touches the lower right corner of the // opaque cell that is above the transparent cell, which is // the upper right corner of the current transparent cell. topVector = new int[]{x * 2 + 1, y * 2 + 1}; } } wasLastCellOpaque = currentIsOpaque; } // Make a note of the lowest opaque-->transparent transition, if there is one. if (wasLastCellOpaque != null && !wasLastCellOpaque.booleanValue()) { queue.addFirst(new Column(x + 1, bottomVector, topVector, octant)); } } // Is the lower-left corner of cell (x,y) within the radius? private boolean IsInRadius(int x, int y) { return (2 * x - 1) * (2 * x - 1) + (2 * y - 1) * (2 * y - 1) <= 4 * viewRange * viewRange; } // Octant helpers // // // \2|1/ // 3\|/0 // ----+---- // 4/|\7 // /5|6\ // // private int[] TranslateOctant(int[] thepos, int octant) { int[] pos = {thepos[0], thepos[1]}; if (octant == 1) { int temp = pos[0]; pos[0] = pos[1]; pos[1] = temp; } else if (octant == 2) { int temp = pos[0]; pos[0] = pos[1]*-1; pos[1] = temp; } else if (octant == 3) { pos[0] = pos[0]*-1; } else if (octant == 4) { int temp = pos[0]*-1; pos[0] = pos[1]*-1; pos[1] = temp; } else if (octant == 5) { pos[1] = pos[1]*-1; pos[0] = pos[0]*-1; } else if (octant == 6) { int temp = pos[1]; pos[1] = pos[0]*-1; pos[0] = temp; } else if (octant == 7) { pos[1] = pos[1]*-1; } pos[0] = (pos[0])+startX; pos[1] = (pos[1])+startY; return pos; } private boolean isOpaque(int x, int y, int octant) { int[] pos = TranslateOctant(new int[]{x,y}, octant); return GameData.level.checkOpaque(GameData.level.getTile(pos[0], pos[1])); } } class Column { private int X; private int[] BottomVector; private int[] TopVector; private int octant; public Column(int x, int[] bottom, int[] top, int octant) { this.setOctant(octant); this.X = x; this.BottomVector = bottom; this.TopVector = top; } public int getX() { return X; } public void setX(int X) { this.X = X; } public int[] getBottomVector() { return BottomVector; } public void setBottomVector(int[] v) { BottomVector = v; } public int[] getTopVector() { return TopVector; } public void setTopVector(int[] v) { TopVector = v; } public int getOctant() { return octant; } public void setOctant(int octant) { this.octant = octant; } }