All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.gurkenlabs.litiengine.entities.LightSource Maven / Gradle / Ivy

The newest version!
package de.gurkenlabs.litiengine.entities;

import de.gurkenlabs.litiengine.Game;
import de.gurkenlabs.litiengine.environment.tilemap.MapObjectProperty;
import de.gurkenlabs.litiengine.environment.tilemap.MapObjectType;
import de.gurkenlabs.litiengine.environment.tilemap.TmxProperty;
import de.gurkenlabs.litiengine.environment.tilemap.TmxType;
import de.gurkenlabs.litiengine.graphics.IRenderable;
import de.gurkenlabs.litiengine.graphics.RenderType;
import de.gurkenlabs.litiengine.graphics.ShapeRenderer;
import de.gurkenlabs.litiengine.util.geom.GeometricUtilities;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.RadialGradientPaint;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.function.Predicate;

/**
 * This class represents a light source in an environment. It extends the Entity class and implements IRenderable interface. It provides methods to
 * render light sources and shadows.
 */
@EntityInfo(renderType = RenderType.OVERLAY)
@TmxType(MapObjectType.LIGHTSOURCE)
public class LightSource extends Entity implements IRenderable {
  /**
   * Enum representing the type of light source.
   */
  public enum Type {
    ELLIPSE,
    RECTANGLE
  }

  public static final String TOGGLE_MESSAGE = "toggle";
  public static final int DEFAULT_INTENSITY = 100;

  private static final float OBSTRUCTED_VISION_RADIUS = 200f;
  private static final float SHADOW_GRADIENT_SIZE = 100f;
  /**
   * The fractions for our shadow gradient, going from 0.0 (black) to 1.0 (transparent).
   */
  private static final float[] SHADOW_GRADIENT_FRACTIONS = new float[] {0f, 1f};
  /**
   * The colors for our shadow, going from opaque black to transparent black.
   */
  private static final Color[] SHADOW_GRADIENT_COLORS =
    new Color[] {new Color(0, 0, 0, .3f), new Color(0f, 0f, 0f, 0f)};

  @TmxProperty(name = MapObjectProperty.LIGHT_ACTIVE)
  private boolean activated;

  @TmxProperty(name = MapObjectProperty.LIGHT_INTENSITY)
  private int intensity;

  @TmxProperty(name = MapObjectProperty.LIGHT_SHAPE)
  private Type lightShapeType;

  private Color color;
  private Shape lightShape;

  /**
   * Constructor for the LightSource class.
   *
   * @param intensity  The intensity of this instance.
   * @param lightColor The color of this instance.
   * @param shapeType  The shape type of this instance.
   * @param activated  A flag indicating whether this light is activated by default.
   */
  public LightSource(final int intensity, final Color lightColor, final Type shapeType, boolean activated) {
    super();
    this.color = lightColor;
    this.intensity = intensity;

    this.lightShapeType = shapeType;
    this.activated = activated;
  }

  /**
   * Activates the light source.
   */
  public void activate() {
    if (isActive()) {
      return;
    }
    this.activated = true;
    updateAmbientLayers();
  }

  /**
   * Deactivates the light source.
   */
  public void deactivate() {
    if (!isActive()) {
      return;
    }

    this.activated = false;
    updateAmbientLayers();
  }

  public Color getColor() {
    return color;
  }

  public RadialGradientPaint getGradientPaint() {
    final Color[] transColors =
      new Color[] {
        getColor().brighter(),
        getColor(),
        new Color(
          getColor().getRed(), getColor().getGreen(), getColor().getBlue(), 0)
      };
    float[] colorFractions = new float[] {0.0f, 0.3f, 1.0f};
    return new RadialGradientPaint(getLightShape().getBounds2D(), colorFractions, transColors, CycleMethod.NO_CYCLE);
  }


  public int getIntensity() {
    return isActive() ? intensity : 0;
  }

  public Shape getLightShape() {
    return lightShape;
  }

  public Type getLightShapeType() {
    return lightShapeType;
  }

  public boolean isActive() {
    return activated;
  }

  public void setColor(final Color color) {
    this.color = color;
    updateAmbientLayers();
  }

  public void setIntensity(final int intensity) {
    this.intensity = intensity;
    updateAmbientLayers();
  }

  public void setLightShapeType(final Type shapeType) {
    this.lightShapeType = shapeType;
  }


  @Override public void setSize(double width, double height) {
    super.setSize(width, height);
    updateShape();
    updateAmbientLayers();
  }

  @Override
  public void setLocation(final Point2D location) {
    super.setLocation(location);
    updateShape();
    updateAmbientLayers();
  }

  /**
   * Toggles the light source between active and inactive states.
   */
  public void toggle() {
    this.activated = !this.activated;
    updateAmbientLayers();
  }

  @Override
  public String sendMessage(final Object sender, final String message) {
    if (message == null || message.isEmpty()) {
      return null;
    }

    if (message.equals(TOGGLE_MESSAGE)) {
      toggle();
      return Boolean.toString(isActive());
    }

    return null;
  }

  @Override
  public void render(final Graphics2D graphic) {
    if (Game.config().graphics().renderDynamicShadows()) {
      renderShadows(graphic);
    }
  }


  /**
   * Updates the ambient layers of the environment.
   */
  private void updateAmbientLayers() {
    if (!isLoaded()) {
      return;
    }

    if (Game.world().environment() != null
      && Game.world().environment().getAmbientLight() != null) {
      Game.world().environment().getAmbientLight().updateSection(getBoundingBox());
    }

    if (Game.world().environment() != null
      && Game.world().environment().getStaticShadowLayer() != null) {
      Game.world().environment().getStaticShadowLayer().updateSection(getBoundingBox());
    }
  }

  /**
   * Updates the shape of the light source based on its type.
   */
  private void updateShape() {
    if (getLightShapeType() == Type.RECTANGLE) {
      this.lightShape =
        new Rectangle2D.Double(getX(), getY(), getWidth(), getHeight());
    } else {
      this.lightShape =
        new Ellipse2D.Double(getX(), getY(), getWidth(), getHeight());
    }
  }

  /**
   * Renders the shadows using simple vector math. The steps are as follows:
   *
   * 
   * for each entity
   *     if entity is not moving:
   *         ignore entity
   *     if entity is too far from mouse:
   *         ignore entity
   *
   *     determine unit vector from mouse to entity center
   *     get perpendicular of unit vector
   *
   *     Create Points A + B:
   *         extrude perpendicular in either direction, by the half-size of the entity
   *     Create Points C + D:
   *         extrude A + B away from mouse location
   *
   *     construct polygon with points A, B, C, D
   *
   *     render with RadialGradientPaint to give it a "fade-out" appearance
   * 
* * @param graphic the graphics to use for rendering */ private void renderShadows(final Graphics2D graphic) { if (Game.world().environment().getCombatEntities().stream() .noneMatch(isInRange(getCenter()))) { return; } // we'll use a radial gradient final Paint gradientPaint = new RadialGradientPaint( Game.world().camera().getViewportDimensionCenter(this), SHADOW_GRADIENT_SIZE, SHADOW_GRADIENT_FRACTIONS, SHADOW_GRADIENT_COLORS); // old Paint object for resetting it later final Paint oldPaint = graphic.getPaint(); graphic.setPaint(gradientPaint); // for each entity for (final ICombatEntity mob : Game.world().environment().getCombatEntities()) { if (mob.isDead() || !isInRange(getCenter()).test(mob)) { continue; } final Shape obstructedVision = getObstructedVisionArea(mob, Game.world().camera().getViewportDimensionCenter(this)); // fill the polygon with the gradient paint ShapeRenderer.render(graphic, obstructedVision); } // reset to old Paint object graphic.setPaint(oldPaint); } private static Predicate isInRange(final Point2D center) { return mob -> new Ellipse2D.Double(center.getX() - LightSource.SHADOW_GRADIENT_SIZE, center.getY() - LightSource.SHADOW_GRADIENT_SIZE, LightSource.SHADOW_GRADIENT_SIZE * 2, LightSource.SHADOW_GRADIENT_SIZE * 2) .contains(mob.getCenter()); } /** * Gets the area of obstructed vision for a given entity. * * @param entity The entity for which to get the obstructed vision area. * @param center The center point of the light source. * @return The area of obstructed vision. */ private static Area getObstructedVisionArea(final IEntity entity, final Point2D center) { final Polygon shadowPolygon = new Polygon(); final Ellipse2D shadowEllipse = getShadowEllipse(entity); final Rectangle2D bounds = shadowEllipse.getBounds2D(); // radius of Entity's bounding circle final float r = (float) bounds.getWidth() / 2f; final float ry = (float) bounds.getHeight() / 2f; // get relative center of entity final Point2D relativeCenter = Game.world() .camera() .getViewportLocation(new Point((int) (bounds.getX() + r), (int) (bounds.getY() + ry))); final double cx = relativeCenter.getX(); final double cy = relativeCenter.getY(); // get direction from light to entity center final double dx = cx - center.getX(); final double dy = cy - center.getY(); // get Euclidean distance from entity to center final double distSq = dx * dx + dy * dy; // avoid sqrt for performance // normalize the direction to a unit vector final float len = (float) Math.sqrt(distSq); double nx = dx; double ny = dy; if (len != 0) { // avoid division by 0 nx /= len; ny /= len; } // get perpendicular of unit vector final double px = -ny; final double py = nx; // our perpendicular points in either direction from radius final Point2D.Double pointA = new Point2D.Double(cx - px * r, cy - py * ry); final Point2D.Double pointB = new Point2D.Double(cx + px * r, cy + py * ry); // project the points by our SHADOW_EXTRUDE amount final Point2D pointC = GeometricUtilities.project(center, pointA, OBSTRUCTED_VISION_RADIUS); final Point2D pointD = GeometricUtilities.project(center, pointB, OBSTRUCTED_VISION_RADIUS); // construct a polygon from our points shadowPolygon.reset(); shadowPolygon.addPoint((int) pointA.getX(), (int) pointA.getY()); shadowPolygon.addPoint((int) pointB.getX(), (int) pointB.getY()); shadowPolygon.addPoint((int) pointD.getX(), (int) pointD.getY()); shadowPolygon.addPoint((int) pointC.getX(), (int) pointC.getY()); final Point2D shadowRenderLocation = Game.world() .camera() .getViewportLocation(new Point2D.Double(shadowEllipse.getX(), shadowEllipse.getY())); final Ellipse2D relativeEllipse = new Ellipse2D.Double( shadowRenderLocation.getX(), shadowRenderLocation.getY(), shadowEllipse.getWidth(), shadowEllipse.getHeight()); final Area ellipseArea = new Area(relativeEllipse); final Area shadowArea = new Area(shadowPolygon); shadowArea.add(ellipseArea); return shadowArea; } private static Ellipse2D getShadowEllipse(final IEntity entity) { final int shadowHeight = (int) (entity.getHeight() / 4); final int shadowWidth = (int) (entity.getWidth() / 3); final int yOffset = (int) entity.getHeight(); final double x = entity.getX() + (entity.getWidth() - shadowWidth) / 2; final double y = entity.getY() + yOffset - shadowHeight / 2.0; return new Ellipse2D.Double(x, y, shadowWidth, shadowHeight); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy