Attempt to separate framework from game impl.

This commit is contained in:
Ondřej Hruška
2014-04-08 14:48:24 +02:00
parent 8e8e15355e
commit 251475557f
101 changed files with 698 additions and 523 deletions
+40
View File
@@ -0,0 +1,40 @@
package mightypork.gamecore;
import mightypork.gamecore.input.InputSystem;
import mightypork.gamecore.render.DisplaySystem;
import mightypork.gamecore.resources.sounds.SoundSystem;
/**
* App interface visible to subsystems
*
* @author MightyPork
*/
public interface AppAccess extends BusAccess {
/**
* @return sound system
*/
abstract SoundSystem snd();
/**
* @return input system
*/
abstract InputSystem input();
/**
* @return display system
*/
abstract DisplaySystem disp();
/**
* Quit to OS<br>
* Destroy app & exit VM
*/
abstract void shutdown();
}
+61
View File
@@ -0,0 +1,61 @@
package mightypork.gamecore;
import mightypork.gamecore.control.bus.EventBus;
import mightypork.gamecore.input.InputSystem;
import mightypork.gamecore.render.DisplaySystem;
import mightypork.gamecore.resources.sounds.SoundSystem;
/**
* App access adapter
*
* @author MightyPork
*/
public class AppAdapter implements AppAccess {
private final AppAccess app;
public AppAdapter(AppAccess app) {
if (app == null) throw new NullPointerException("AppAccess instance cannot be null.");
this.app = app;
}
@Override
public final SoundSystem snd()
{
return app.snd();
}
@Override
public final InputSystem input()
{
return app.input();
}
@Override
public final DisplaySystem disp()
{
return app.disp();
}
@Override
public final EventBus bus()
{
return app.bus();
}
@Override
public void shutdown()
{
app.shutdown();
}
}
+14
View File
@@ -0,0 +1,14 @@
package mightypork.gamecore;
import mightypork.gamecore.control.bus.EventBus;
public interface BusAccess {
/**
* @return event bus
*/
public abstract EventBus bus();
}
+66
View File
@@ -0,0 +1,66 @@
package mightypork.gamecore;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.control.bus.events.MainLoopTaskRequest;
import mightypork.gamecore.control.bus.events.UpdateEvent;
import mightypork.gamecore.control.timing.TimerDelta;
public abstract class GameLoop extends Subsystem implements MainLoopTaskRequest.Listener {
private final Queue<Runnable> taskQueue = new ConcurrentLinkedQueue<Runnable>();
/** timer */
private TimerDelta timer;
private boolean running = true;
public GameLoop(AppAccess app) {
super(app);
}
public void start()
{
timer = new TimerDelta();
while (running) {
disp().beginFrame();
bus().send(new UpdateEvent(timer.getDelta()));
Runnable r;
while ((r = taskQueue.poll()) != null) {
r.run();
}
tick();
disp().endFrame();
}
}
/**
* Called each frame, in rendering context.
*/
protected abstract void tick();
@Override
protected final void deinit()
{
running = false;
}
@Override
public final synchronized void queueTask(Runnable request)
{
taskQueue.add(request);
}
}
@@ -0,0 +1,67 @@
package mightypork.gamecore;
import mightypork.utils.logging.LogInstance;
import org.newdawn.slick.util.LogSystem;
public class SlickLogRedirector implements LogSystem {
LogInstance l;
public SlickLogRedirector(LogInstance log) {
this.l = log;
}
@Override
public void error(String msg, Throwable e)
{
l.e(msg, e);
}
@Override
public void error(Throwable e)
{
l.e(e);
}
@Override
public void error(String msg)
{
l.e(msg);
}
@Override
public void warn(String msg)
{
l.w(msg);
}
@Override
public void warn(String msg, Throwable e)
{
l.e(msg, e);
}
@Override
public void info(String msg)
{
l.i(msg);
}
@Override
public void debug(String msg)
{
l.f3(msg);
}
}
@@ -0,0 +1,128 @@
package mightypork.gamecore.control;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import mightypork.gamecore.BusAccess;
import mightypork.gamecore.control.bus.EventBus;
import mightypork.gamecore.control.bus.clients.DelegatingClient;
import mightypork.gamecore.control.bus.clients.ToggleableClient;
import mightypork.gamecore.control.interf.Destroyable;
/**
* Client that can be attached to the {@link EventBus}, or added as a child
* client to another {@link DelegatingClient}
*
* @author MightyPork
*/
public abstract class ChildClient implements BusAccess, DelegatingClient, ToggleableClient, Destroyable {
private BusAccess busAccess;
private final Set<Object> clients = new LinkedHashSet<Object>();
private boolean listening = true;
private boolean delegating = true;
public ChildClient(BusAccess busAccess) {
this.busAccess = busAccess;
bus().subscribe(this);
}
@Override
public final void destroy()
{
deinit();
bus().unsubscribe(this);
}
/**
* Deinitialize the subsystem<br>
* (called during destruction)
*/
protected abstract void deinit();
@Override
public final Collection<Object> getChildClients()
{
return clients;
}
@Override
public final boolean doesDelegate()
{
return delegating;
}
@Override
public boolean isListening()
{
return listening;
}
/**
* Add a child subscriber to the {@link EventBus}.<br>
*
* @param client
*/
public final void addChildClient(Object client)
{
if (bus().isClientValid(client)) {
clients.add(client);
}
}
/**
* Remove a child subscriber
*
* @param client subscriber to remove
*/
public final void removeChildClient(Object client)
{
if (client != null) {
clients.remove(client);
}
}
/**
* Set whether events should be received.
*
* @param listening receive events
*/
public final void setListening(boolean listening)
{
this.listening = listening;
}
/**
* Set whether events should be passed on to child nodes
*
* @param delegating
*/
public final void setDelegating(boolean delegating)
{
this.delegating = delegating;
}
@Override
public EventBus bus()
{
return busAccess.bus();
}
}
@@ -0,0 +1,55 @@
package mightypork.gamecore.control;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.input.InputSystem;
import mightypork.gamecore.render.DisplaySystem;
import mightypork.gamecore.resources.sounds.SoundSystem;
/**
* App event bus client, to be used for subsystems, screens and anything that
* needs access to the eventbus
*
* @author MightyPork
*/
public abstract class Subsystem extends ChildClient implements AppAccess {
private final AppAccess app;
public Subsystem(AppAccess app) {
super(app);
this.app = app;
}
@Override
public final SoundSystem snd()
{
return app.snd();
}
@Override
public final InputSystem input()
{
return app.input();
}
@Override
public final DisplaySystem disp()
{
return app.disp();
}
@Override
public void shutdown()
{
app.shutdown();
}
}
@@ -0,0 +1,130 @@
package mightypork.gamecore.control.bus;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
/**
* HashSet that buffers add and remove calls and performs them all at once when
* a flush() method is called.
*
* @author MightyPork
* @param <E> element type
*/
public class BufferedHashSet<E> extends HashSet<E> {
private final List<E> toAdd = new LinkedList<E>();
private final List<Object> toRemove = new LinkedList<Object>();
private boolean buffering = false;
public BufferedHashSet() {
super();
}
public BufferedHashSet(Collection<? extends E> c) {
super(c);
}
public BufferedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
public BufferedHashSet(int initialCapacity) {
super(initialCapacity);
}
@Override
public boolean add(E e)
{
if (buffering) {
toAdd.add(e);
} else {
super.add(e);
}
return true;
}
@Override
public boolean remove(Object e)
{
if (buffering) {
toRemove.add(e);
} else {
super.remove(e);
}
return true;
}
/**
* Flush all
*/
private void flush()
{
for (final E e : toAdd) {
super.add(e);
}
for (final Object e : toRemove) {
super.remove(e);
}
toAdd.clear();
toRemove.clear();
}
@Override
public boolean removeAll(Collection<?> c)
{
throw new UnsupportedOperationException();
}
@Override
public boolean containsAll(Collection<?> c)
{
throw new UnsupportedOperationException();
}
@Override
public boolean addAll(Collection<? extends E> c)
{
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(Collection<?> c)
{
throw new UnsupportedOperationException();
}
/**
* Toggle buffering
*
* @param enable enable buffering
*/
public void setBuffering(boolean enable)
{
if (this.buffering && !enable) {
flush();
}
this.buffering = enable;
}
}
@@ -0,0 +1,359 @@
package mightypork.gamecore.control.bus;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import mightypork.gamecore.control.bus.events.Event;
import mightypork.gamecore.control.bus.events.types.DelayedEvent;
import mightypork.gamecore.control.bus.events.types.ImmediateEvent;
import mightypork.gamecore.control.bus.events.types.SingleReceiverEvent;
import mightypork.gamecore.control.bus.events.types.UnloggedEvent;
import mightypork.gamecore.control.interf.Destroyable;
import mightypork.utils.logging.Log;
/**
* An event bus, accommodating multiple {@link EventChannel}s.
*
* @author MightyPork
*/
final public class EventBus implements Destroyable {
/** Message channels */
private final BufferedHashSet<EventChannel<?, ?>> channels = new BufferedHashSet<EventChannel<?, ?>>();
/** Registered clients */
private final BufferedHashSet<Object> clients = new BufferedHashSet<Object>();
/** Messages queued for delivery */
private final DelayQueue<DelayQueueEntry> sendQueue = new DelayQueue<DelayQueueEntry>();
/** Queue polling thread */
private final QueuePollingThread busThread;
/** Whether the bus was destroyed */
private boolean dead = false;
public boolean logSending = false;
/**
* Make a new bus and start it's queue thread.
*/
public EventBus() {
busThread = new QueuePollingThread();
busThread.start();
}
private boolean shallLog(Event<?> event)
{
if (!logSending) return false;
if (event.getClass().isAnnotationPresent(UnloggedEvent.class)) return false;
return true;
}
/**
* Add a {@link EventChannel} to this bus.<br>
* If a channel of matching types is already added, it is returned instead.
*
* @param channel channel to be added
* @return the channel that's now in the bus
*/
public EventChannel<?, ?> addChannel(EventChannel<?, ?> channel)
{
assertLive();
// if the channel already exists, return this instance instead.
for (final EventChannel<?, ?> ch : channels) {
if (ch.equals(channel)) {
Log.w("<bus> Channel of type " + Log.str(channel) + " already registered.");
return ch;
}
}
channels.add(channel);
return channel;
}
/**
* Add a channel for given event and client type.
*
* @param eventClass event type
* @param clientClass client type
* @return the created channel instance
*/
public <F_EVENT extends Event<F_CLIENT>, F_CLIENT> EventChannel<?, ?> addChannel(Class<F_EVENT> eventClass, Class<F_CLIENT> clientClass)
{
assertLive();
final EventChannel<F_EVENT, F_CLIENT> channel = EventChannel.create(eventClass, clientClass);
return addChannel(channel);
}
/**
* Remove a {@link EventChannel} from this bus
*
* @param channel true if channel was removed
*/
public void removeChannel(EventChannel<?, ?> channel)
{
assertLive();
channels.remove(channel);
}
/**
* Send based on annotation.
*
* @param event event
*/
public void send(Event<?> event)
{
assertLive();
final DelayedEvent adelay = event.getClass().getAnnotation(DelayedEvent.class);
if (adelay != null) {
sendDelayed(event, adelay.delay());
return;
}
if (event.getClass().isAnnotationPresent(ImmediateEvent.class)) {
sendDirect(event);
return;
}
sendQueued(event);
}
/**
* Add event to a queue
*
* @param event event
*/
public void sendQueued(Event<?> event)
{
assertLive();
sendDelayed(event, 0);
}
/**
* Add event to a queue, scheduled for given time.
*
* @param event event
* @param delay delay before event is dispatched
*/
public void sendDelayed(Event<?> event, double delay)
{
assertLive();
final DelayQueueEntry dm = new DelayQueueEntry(delay, event);
if (shallLog(event)) Log.f3("<bus> Qu " + Log.str(event) + ", t = +" + delay + "s");
sendQueue.add(dm);
}
/**
* Send immediately.<br>
* Should be used for real-time events that require immediate response, such
* as timing events.
*
* @param event event
*/
public void sendDirect(Event<?> event)
{
assertLive();
if (shallLog(event)) Log.f3("<bus> Di " + Log.str(event));
dispatch(event);
}
/**
* Send immediately.<br>
* Should be used for real-time events that require immediate response, such
* as timing events.
*
* @param event event
*/
private void dispatch(Event<?> event)
{
assertLive();
synchronized (this) {
channels.setBuffering(true);
clients.setBuffering(true);
boolean sent = false;
boolean accepted = false;
final boolean singular = event.getClass().isAnnotationPresent(SingleReceiverEvent.class);
for (final EventChannel<?, ?> b : channels) {
if (b.canBroadcast(event)) {
accepted = true;
sent |= b.broadcast(event, clients);
}
if (sent && singular) break;
}
if (!accepted) Log.e("<bus> Not accepted by any channel: " + Log.str(event));
if (!sent && shallLog(event)) Log.w("<bus> Not delivered: " + Log.str(event));
channels.setBuffering(false);
clients.setBuffering(false);
}
}
/**
* Connect a client to the bus. The client will be connected to all current
* and future channels, until removed from the bus.
*
* @param client the client
*/
public void subscribe(Object client)
{
assertLive();
if (client == null) return;
clients.add(client);
}
/**
* Disconnect a client from the bus.
*
* @param client the client
*/
public void unsubscribe(Object client)
{
assertLive();
clients.remove(client);
}
public boolean isClientValid(Object client)
{
assertLive();
if (client == null) return false;
for (final EventChannel<?, ?> ch : channels) {
if (ch.isClientValid(client)) {
return true;
}
}
return false;
}
private class DelayQueueEntry implements Delayed {
private final long due;
private Event<?> evt = null;
public DelayQueueEntry(double seconds, Event<?> event) {
super();
this.due = System.currentTimeMillis() + (long) (seconds * 1000);
this.evt = event;
}
@Override
public int compareTo(Delayed o)
{
return -Long.valueOf(o.getDelay(TimeUnit.MILLISECONDS)).compareTo(getDelay(TimeUnit.MILLISECONDS));
}
@Override
public long getDelay(TimeUnit unit)
{
return unit.convert(due - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
public Event<?> getEvent()
{
return evt;
}
}
private class QueuePollingThread extends Thread {
public boolean stopped = false;
public QueuePollingThread() {
super("Queue Polling Thread");
}
@Override
public void run()
{
DelayQueueEntry evt;
while (!stopped) {
evt = null;
try {
evt = sendQueue.take();
} catch (final InterruptedException ignored) {
//
}
if (evt != null) {
dispatch(evt.getEvent());
}
}
}
}
/**
* Halt bus thread and reject any future events.
*/
@Override
public void destroy()
{
assertLive();
busThread.stopped = true;
dead = true;
}
/**
* Make sure the bus is not destroyed.
*
* @throws IllegalStateException if the bus is dead.
*/
private void assertLive() throws IllegalStateException
{
if (dead) throw new IllegalStateException("EventBus is dead.");
}
}
@@ -0,0 +1,215 @@
package mightypork.gamecore.control.bus;
import java.util.Collection;
import java.util.HashSet;
import mightypork.gamecore.control.bus.clients.DelegatingClient;
import mightypork.gamecore.control.bus.clients.ToggleableClient;
import mightypork.gamecore.control.bus.events.Event;
import mightypork.gamecore.control.bus.events.types.SingleReceiverEvent;
import mightypork.utils.logging.Log;
/**
* Event delivery channel, module of {@link EventBus}
*
* @author MightyPork
* @param <EVENT> event type
* @param <CLIENT> client (subscriber) type
*/
final public class EventChannel<EVENT extends Event<CLIENT>, CLIENT> {
private final Class<CLIENT> clientClass;
private final Class<EVENT> eventClass;
/**
* Create a channel
*
* @param eventClass event class
* @param clientClass client class
*/
public EventChannel(Class<EVENT> eventClass, Class<CLIENT> clientClass) {
if (eventClass == null || clientClass == null) {
throw new NullPointerException("Null Event or Client class.");
}
this.clientClass = clientClass;
this.eventClass = eventClass;
}
/**
* Try to broadcast a event.<br>
* If event is of wrong type, <code>false</code> is returned.
*
* @param event a event to be sent
* @param clients collection of clients
* @return true if event was sent
*/
public boolean broadcast(Event<?> event, Collection<Object> clients)
{
if (!canBroadcast(event)) return false;
return doBroadcast(eventClass.cast(event), clients, new HashSet<Object>());
}
/**
* Send the event
*
* @param event sent event
* @param clients subscribing clients
* @param processed clients already processed
* @return success
*/
private boolean doBroadcast(final EVENT event, final Collection<Object> clients, final Collection<Object> processed)
{
boolean sent = false;
final boolean singular = event.getClass().isAnnotationPresent(SingleReceiverEvent.class);
for (final Object client : clients) {
// exclude obvious non-clients
if (!isClientValid(client)) {
continue;
}
// avoid executing more times
if (processed.contains(client)) {
Log.w("<bus> Client already served: " + Log.str(client));
continue;
}
processed.add(client);
// opt-out
if (client instanceof ToggleableClient) {
if (!((ToggleableClient) client).isListening()) continue;
}
sent |= sendTo(client, event);
// singular event ain't no whore, handled once only.
if (sent && singular) return true;
// pass on to delegated clients
if (client instanceof DelegatingClient) {
if (((DelegatingClient) client).doesDelegate()) {
final Collection<Object> children = ((DelegatingClient) client).getChildClients();
if (children != null && children.size() > 0) {
sent |= doBroadcast(event, children, processed);
}
}
}
}
return sent;
}
/**
* Send an event to a client.
*
* @param client target client
* @param event event to send
* @return success
*/
@SuppressWarnings("unchecked")
private boolean sendTo(Object client, EVENT event)
{
if (isClientOfType(client)) {
((Event<CLIENT>) event).handleBy((CLIENT) client);
return true;
}
return false;
}
/**
* Check if the given event can be broadcasted by this {@link EventChannel}
*
* @param event event object
* @return can be broadcasted
*/
public boolean canBroadcast(Event<?> event)
{
return event != null && eventClass.isInstance(event);
}
/**
* Create an instance for given types
*
* @param eventClass event class
* @param clientClass client class
* @return the broadcaster
*/
public static <F_EVENT extends Event<F_CLIENT>, F_CLIENT> EventChannel<F_EVENT, F_CLIENT> create(Class<F_EVENT> eventClass, Class<F_CLIENT> clientClass)
{
return new EventChannel<F_EVENT, F_CLIENT>(eventClass, clientClass);
}
/**
* Check if client is of channel type
*
* @param client client
* @return is of type
*/
private boolean isClientOfType(Object client)
{
return clientClass.isInstance(client);
}
/**
* Check if the channel is compatible with given
*
* @param client client
* @return is supported
*/
public boolean isClientValid(Object client)
{
return isClientOfType(client) || (client instanceof DelegatingClient);
}
@Override
public int hashCode()
{
final int prime = 13;
int result = 1;
result = prime * result + ((clientClass == null) ? 0 : clientClass.hashCode());
result = prime * result + ((eventClass == null) ? 0 : eventClass.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof EventChannel)) return false;
final EventChannel<?, ?> other = (EventChannel<?, ?>) obj;
if (clientClass == null) {
if (other.clientClass != null) return false;
} else if (!clientClass.equals(other.clientClass)) return false;
if (eventClass == null) {
if (other.eventClass != null) return false;
} else if (!eventClass.equals(other.eventClass)) return false;
return true;
}
@Override
public String toString()
{
return "{ " + Log.str(eventClass) + " => " + Log.str(clientClass) + " }";
}
}
@@ -0,0 +1,25 @@
package mightypork.gamecore.control.bus.clients;
import java.util.Collection;
/**
* Client containing child clients
*
* @author MightyPork
*/
public interface DelegatingClient {
/**
* @return collection of child clients. Can not be null.
*/
public Collection<Object> getChildClients();
/**
* @return true if delegating is active
*/
public boolean doesDelegate();
}
@@ -0,0 +1,16 @@
package mightypork.gamecore.control.bus.clients;
/**
* Client that can toggle receiving messages.
*
* @author MightyPork
*/
public interface ToggleableClient {
/**
* @return true if the client wants to receive messages
*/
public boolean isListening();
}
@@ -0,0 +1,22 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.ImmediateEvent;
import mightypork.gamecore.control.interf.Destroyable;
/**
* Invoke destroy() method of all subscribers. Used to deinit a system.
*
* @author MightyPork
*/
@ImmediateEvent
public class DestroyEvent implements Event<Destroyable> {
@Override
public void handleBy(Destroyable handler)
{
handler.destroy();
}
}
@@ -0,0 +1,34 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.DelayedEvent;
import mightypork.gamecore.control.bus.events.types.ImmediateEvent;
import mightypork.gamecore.control.bus.events.types.SingleReceiverEvent;
/**
* <p>
* Something that can be handled by HANDLER.
* </p>
* <p>
* Can be annotated as {@link SingleReceiverEvent} to be delivered once only,
* and {@link DelayedEvent} or {@link ImmediateEvent} to specify default sending
* mode.
* </p>
* <p>
* Default sending mode (if not changed by annotations) is <i>queued</i> with
* zero delay.
* </p>
*
* @author MightyPork
* @param <HANDLER> handler type
*/
public interface Event<HANDLER> {
/**
* Ask handler to handle this message.
*
* @param handler handler instance
*/
public void handleBy(HANDLER handler);
}
@@ -0,0 +1,85 @@
package mightypork.gamecore.control.bus.events;
import org.lwjgl.input.Keyboard;
/**
* A keyboard event
*
* @author MightyPork
*/
public class KeyboardEvent implements Event<KeyboardEvent.Listener> {
private final int key;
private final boolean down;
private final char c;
public KeyboardEvent(int key, char c, boolean down) {
this.key = key;
this.c = c;
this.down = down;
}
/**
* @return key code (see {@link org.lwjgl.input.Keyboard})
*/
public int getKey()
{
return key;
}
/**
* @return true if key was just pressed
*/
public boolean isDown()
{
return down;
}
/**
* @return true if key was just released
*/
public boolean isUp()
{
return !down;
}
/**
* @return event character (if any)
*/
public char getChar()
{
return c;
}
@Override
public void handleBy(Listener keh)
{
keh.receive(this);
}
public interface Listener {
/**
* Handle an event
*
* @param event event
*/
void receive(KeyboardEvent event);
}
@Override
public String toString()
{
return Keyboard.getKeyName(key) + ":" + (down ? "DOWN" : "UP");
}
}
@@ -0,0 +1,38 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.SingleReceiverEvent;
/**
* Request to execute given {@link Runnable} in main loop.
*
* @author MightyPork
*/
@SingleReceiverEvent
public class MainLoopTaskRequest implements Event<MainLoopTaskRequest.Listener> {
private final Runnable task;
public MainLoopTaskRequest(Runnable task) {
this.task = task;
}
@Override
public void handleBy(Listener handler)
{
handler.queueTask(task);
}
public interface Listener {
/**
* Perform the requested action
*
* @param request
*/
void queueTask(Runnable request);
}
}
@@ -0,0 +1,131 @@
package mightypork.gamecore.control.bus.events;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Coord;
/**
* Mouse button / wheel event triggered
*
* @author MightyPork
*/
public class MouseButtonEvent implements Event<MouseButtonEvent.Listener> {
public static final int BUTTON_LEFT = 0;
public static final int BUTTON_MIDDLE = 1;
public static final int BUTTON_RIGHT = 2;
private final int button;
private final int wheeld;
private final Coord pos;
private final boolean down;
/**
* Mouse button event
*
* @param pos event position
* @param button button id
* @param down button pressed
* @param wheeld wheel change
*/
public MouseButtonEvent(Coord pos, int button, boolean down, int wheeld) {
this.button = button;
this.down = down;
this.pos = pos;
this.wheeld = wheeld;
}
/**
* @return true if the event was caused by a button state change
*/
public boolean isButtonEvent()
{
return button != -1;
}
/**
* @return true if the event was caused by a wheel change
*/
public boolean isWheelEvent()
{
return wheeld != 0;
}
/**
* @return button id or -1 if none was pressed
*/
public int getButton()
{
return button;
}
/**
* @return number of steps the wheel changed since last event
*/
public int getWheelDelta()
{
return wheeld;
}
/**
* @return mouse position when the event occurred
*/
public Coord getPos()
{
return pos;
}
/**
* @return true if button was just pressed
*/
public boolean isDown()
{
return button != -1 && down;
}
/**
* @return true if button was just released
*/
public boolean isUp()
{
return button != -1 && !down;
}
/**
* Get if event happened over a rect
*
* @param rect rect region
* @return was over
*/
public boolean isOver(RectConstraint rect)
{
return pos.isInRect(rect.getRect());
}
@Override
public void handleBy(Listener handler)
{
handler.receive(this);
}
public interface Listener {
/**
* Handle an event
*
* @param event event
*/
void receive(MouseButtonEvent event);
}
}
@@ -0,0 +1,60 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.UnloggedEvent;
import mightypork.utils.math.coord.Coord;
/**
* Mouse moved
*
* @author MightyPork
*/
@UnloggedEvent
public class MouseMotionEvent implements Event<MouseMotionEvent.Listener> {
private final Coord move;
private final Coord pos;
public MouseMotionEvent(Coord pos, Coord move) {
this.move = move;
this.pos = pos;
}
/**
* @return movement since last {@link MouseMotionEvent}
*/
public Coord getMove()
{
return move;
}
/**
* @return current mouse position
*/
public Coord getPos()
{
return pos;
}
@Override
public void handleBy(Listener keh)
{
keh.receive(this);
}
public interface Listener {
/**
* Handle an event
*
* @param event event
*/
void receive(MouseMotionEvent event);
}
}
@@ -0,0 +1,39 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.SingleReceiverEvent;
import mightypork.gamecore.loading.DeferredResource;
/**
* Request to schedule loading a deferred resource.
*
* @author MightyPork
*/
@SingleReceiverEvent
public class ResourceLoadRequest implements Event<ResourceLoadRequest.Listener> {
private final DeferredResource resource;
public ResourceLoadRequest(DeferredResource resource) {
this.resource = resource;
}
@Override
public void handleBy(Listener handler)
{
handler.loadResource(resource);
}
public interface Listener {
/**
* Load a resource
*
* @param resource
*/
void loadResource(DeferredResource resource);
}
}
@@ -0,0 +1,54 @@
package mightypork.gamecore.control.bus.events;
import mightypork.utils.math.coord.Coord;
/**
* Screen resolution or mode was changed
*
* @author MightyPork
*/
public class ScreenChangeEvent implements Event<ScreenChangeEvent.Listener> {
private final boolean fullscreen;
private final Coord screenSize;
private final boolean fsChanged;
public ScreenChangeEvent(boolean fsChanged, boolean fullscreen, Coord size) {
this.fullscreen = fullscreen;
this.screenSize = size;
this.fsChanged = fsChanged;
}
public boolean isFullscreen()
{
return fullscreen;
}
public boolean fullscreenChanged()
{
return fsChanged;
}
public Coord getScreenSize()
{
return screenSize;
}
@Override
public void handleBy(Listener handler)
{
handler.receive(this);
}
public interface Listener {
void receive(ScreenChangeEvent event);
}
}
@@ -0,0 +1,34 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.SingleReceiverEvent;
/**
* Request to change screen
*
* @author MightyPork
*/
@SingleReceiverEvent
public class ScreenRequestEvent implements Event<ScreenRequestEvent.Listener> {
private final String scrName;
public ScreenRequestEvent(String screenKey) {
scrName = screenKey;
}
@Override
public void handleBy(Listener handler)
{
handler.showScreen(scrName);
}
public interface Listener {
void showScreen(String key);
}
}
@@ -0,0 +1,35 @@
package mightypork.gamecore.control.bus.events;
import mightypork.gamecore.control.bus.events.types.ImmediateEvent;
import mightypork.gamecore.control.bus.events.types.UnloggedEvent;
import mightypork.gamecore.control.interf.Updateable;
/**
* Delta timing update event
*
* @author MightyPork
*/
// sending via queue would hog the bus
@ImmediateEvent
@UnloggedEvent
public class UpdateEvent implements Event<Updateable> {
private final double deltaTime;
/**
* @param deltaTime time since last update (sec)
*/
public UpdateEvent(double deltaTime) {
this.deltaTime = deltaTime;
}
@Override
public void handleBy(Updateable handler)
{
handler.update(deltaTime);
}
}
@@ -0,0 +1,22 @@
package mightypork.gamecore.control.bus.events.types;
import java.lang.annotation.*;
/**
* Event that should be queued with given delay (default: 0);
*
* @author MightyPork
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface DelayedEvent {
/**
* @return event dispatch delay [seconds]
*/
double delay() default 0;
}
@@ -0,0 +1,16 @@
package mightypork.gamecore.control.bus.events.types;
import java.lang.annotation.*;
/**
* Event that should not be queued.
*
* @author MightyPork
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface ImmediateEvent {}
@@ -0,0 +1,16 @@
package mightypork.gamecore.control.bus.events.types;
import java.lang.annotation.*;
/**
* Handled only by the first client, then discarded.
*
* @author MightyPork
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface SingleReceiverEvent {}
@@ -0,0 +1,17 @@
package mightypork.gamecore.control.bus.events.types;
import java.lang.annotation.*;
/**
* Event that's not worth logging, unless there was an error with it.<br>
* Useful for common events that would otherwise clutter the log.
*
* @author MightyPork
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface UnloggedEvent {}
@@ -0,0 +1,15 @@
package mightypork.gamecore.control.interf;
/**
* Object that can be destroyed (free resources etc)
*
* @author MightyPork
*/
public interface Destroyable {
/**
* Destroy this object
*/
public void destroy();
}
@@ -0,0 +1,17 @@
package mightypork.gamecore.control.interf;
/**
* Uses delta timing
*
* @author MightyPork
*/
public interface Updateable {
/**
* Update item state based on elapsed time
*
* @param delta time elapsed since last update, in seconds
*/
public void update(double delta);
}
@@ -0,0 +1,62 @@
package mightypork.gamecore.control.timing;
/**
* Class for counting FPS in games.<br>
* This class can be used also as a simple frequency meter - output is in Hz.
*
* @author MightyPork
*/
public class FpsMeter {
private long frames = 0;
private long drops = 0;
private long lastTimeMillis = System.currentTimeMillis();
private long lastSecFPS = 0;
private long lastSecDrop = 0;
/**
* @return current second's FPS
*/
public long getFPS()
{
return lastSecFPS;
}
/**
* Notification that frame was rendered
*/
public void frame()
{
if (System.currentTimeMillis() - lastTimeMillis > 1000) {
lastSecFPS = frames;
lastSecDrop = drops;
frames = 0;
drops = 0;
lastTimeMillis = System.currentTimeMillis();
}
frames++;
}
/**
* Notification that some frames have been dropped
*
* @param dropped dropped frames
*/
public void drop(int dropped)
{
drops += dropped;
}
/**
* @return current second's dropped frames
*/
public long getDropped()
{
return lastSecDrop;
}
}
@@ -0,0 +1,23 @@
package mightypork.gamecore.control.timing;
public interface Pauseable {
/**
* Pause operation
*/
public void pause();
/**
* Resume operation
*/
public void resume();
/**
* @return paused state
*/
public boolean isPaused();
}
@@ -0,0 +1,47 @@
package mightypork.gamecore.control.timing;
/**
* Timer for delta timing
*
* @author MightyPork
*/
public class TimerDelta {
private long lastFrame;
private static final long SECOND = 1000000000; // a million nanoseconds
/**
* New delta timer
*/
public TimerDelta() {
lastFrame = System.nanoTime();
}
/**
* Get current time in NS
*
* @return current time NS
*/
public long getTime()
{
return System.nanoTime();
}
/**
* Get time since the last "getDelta()" call.
*
* @return delta time (seconds)
*/
public double getDelta()
{
final long time = getTime();
final double delta = (time - lastFrame) / (double) SECOND;
lastFrame = time;
return delta;
}
}
@@ -0,0 +1,103 @@
package mightypork.gamecore.control.timing;
/**
* Timer for interpolated timing
*
* @author MightyPork
*/
public class TimerFps {
private long lastFrame = 0;
private long nextFrame = 0;
private long skipped = 0;
private long lastSkipped = 0;
private static final long SECOND = 1000000000; // a million nanoseconds
private final long FRAME; // a time of one frame in nanoseconds
/**
* New interpolated timer
*
* @param fps target FPS
*/
public TimerFps(long fps) {
FRAME = Math.round(SECOND / fps);
lastFrame = System.nanoTime();
nextFrame = System.nanoTime() + FRAME;
}
/**
* Sync and calculate dropped frames etc.
*/
public void sync()
{
final long time = getTime();
if (time >= nextFrame) {
final long skippedNow = (long) Math.floor((time - nextFrame) / (double) FRAME) + 1;
skipped += skippedNow;
lastFrame = nextFrame + (1 - skippedNow) * FRAME;
nextFrame += skippedNow * FRAME;
}
}
/**
* Get nanotime
*
* @return nanotime
*/
public long getTime()
{
return System.nanoTime();
}
/**
* Get fraction of next frame
*
* @return fraction
*/
public double getFraction()
{
if (getSkipped() >= 1) {
return 1;
}
final long time = getTime();
if (time <= nextFrame) {
return (double) (time - lastFrame) / (double) FRAME;
}
return 1;
}
/**
* Get number of elapsed ticks
*
* @return ticks
*/
public int getSkipped()
{
final long change = skipped - lastSkipped;
lastSkipped = skipped;
return (int) change;
}
/**
* Clear timer and start counting new tick.
*/
public void startNewFrame()
{
final long time = getTime();
lastFrame = time;
nextFrame = time + FRAME;
lastSkipped = skipped;
}
}
@@ -0,0 +1,42 @@
package mightypork.gamecore.gui.renderers;
import static mightypork.utils.math.constraints.ConstraintFactory.*;
import mightypork.gamecore.AppAccess;
import mightypork.utils.math.constraints.RectConstraint;
public class ColumnHolder extends ElementHolder {
private final int cols;
private int col = 0;
public ColumnHolder(AppAccess app, RectConstraint context, int rows) {
super(app, context);
this.cols = rows;
}
public ColumnHolder(AppAccess app, int rows) {
super(app);
this.cols = rows;
}
/**
* Add a row to the holder.
*
* @param elem
*/
@Override
public void add(final PluggableRenderable elem)
{
if (elem == null) return;
elem.setContext(c_column(this, cols, col++));
attach(elem);
}
}
@@ -0,0 +1,89 @@
package mightypork.gamecore.gui.renderers;
import java.util.LinkedList;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.control.ChildClient;
import mightypork.gamecore.control.bus.EventBus;
import mightypork.gamecore.render.Renderable;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Rect;
/**
* Bag for {@link PluggableRenderer} elements with constraints.<br>
* Elements are exposed to {@link EventBus}.
*
* @author MightyPork
*/
public abstract class ElementHolder extends ChildClient implements PluggableRenderable {
private final LinkedList<PluggableRenderable> elements = new LinkedList<PluggableRenderable>();
private RectConstraint context;
public ElementHolder(AppAccess app) {
super(app);
}
public ElementHolder(AppAccess app, RectConstraint context) {
super(app);
setContext(context);
}
@Override
public final void setContext(RectConstraint context)
{
this.context = context;
}
@Override
public final void render()
{
for (final Renderable element : elements) {
element.render();
}
}
@Override
public final Rect getRect()
{
return context.getRect();
}
/**
* Add element to the holder, setting it's context.<br>
* Element must then be attached using the <code>attach</code> method.
*
* @param elem element
*/
public abstract void add(PluggableRenderable elem);
/**
* Connect to bus and add to element list
*
* @param elem element; it's context will be set to the constraint.
*/
public final void attach(PluggableRenderable elem)
{
if (elem == null) return;
elements.add(elem);
addChildClient(elem);
}
@Override
protected void deinit()
{
// no impl
}
}
@@ -0,0 +1,30 @@
package mightypork.gamecore.gui.renderers;
import mightypork.gamecore.render.Render;
import mightypork.gamecore.resources.textures.TxQuad;
public class ImageRenderer extends PluggableRenderer {
private TxQuad texture;
public ImageRenderer(TxQuad texture) {
this.texture = texture;
}
public void setTexture(TxQuad texture)
{
this.texture = texture;
}
@Override
public void render()
{
Render.quadTextured(getRect(), texture);
}
}
@@ -0,0 +1,23 @@
package mightypork.gamecore.gui.renderers;
import mightypork.gamecore.render.Renderable;
import mightypork.utils.math.constraints.PluggableContext;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Rect;
public interface PluggableRenderable extends Renderable, PluggableContext {
@Override
void render();
@Override
Rect getRect();
@Override
void setContext(RectConstraint rect);
}
@@ -0,0 +1,33 @@
package mightypork.gamecore.gui.renderers;
import mightypork.gamecore.render.Renderable;
import mightypork.utils.math.constraints.ContextAdapter;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Rect;
/**
* {@link Renderable} with pluggable context
*
* @author MightyPork
*/
public abstract class PluggableRenderer extends ContextAdapter implements PluggableRenderable {
@Override
public abstract void render();
@Override
public Rect getRect()
{
return super.getRect();
}
@Override
public void setContext(RectConstraint rect)
{
super.setContext(rect);
}
}
@@ -0,0 +1,42 @@
package mightypork.gamecore.gui.renderers;
import static mightypork.utils.math.constraints.ConstraintFactory.*;
import mightypork.gamecore.AppAccess;
import mightypork.utils.math.constraints.RectConstraint;
public class RowHolder extends ElementHolder {
private final int rows;
private int row = 0;
public RowHolder(AppAccess app, RectConstraint context, int rows) {
super(app, context);
this.rows = rows;
}
public RowHolder(AppAccess app, int rows) {
super(app);
this.rows = rows;
}
/**
* Add a row to the holder.
*
* @param elem
*/
@Override
public void add(final PluggableRenderable elem)
{
if (elem == null) return;
elem.setContext(c_row(this, rows, row++));
attach(elem);
}
}
@@ -0,0 +1,82 @@
package mightypork.gamecore.gui.renderers;
import mightypork.gamecore.resources.fonts.FontRenderer;
import mightypork.gamecore.resources.fonts.GLFont;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
public class TextRenderer extends PluggableRenderer {
public static enum Align
{
LEFT, CENTER, RIGHT;
}
private FontRenderer font;
private String text;
private RGB color;
private Align align;
public TextRenderer(GLFont font, String text, RGB color, Align align) {
this.font = new FontRenderer(font);
this.text = text;
this.color = color;
this.align = align;
}
public void setFont(FontRenderer font)
{
this.font = font;
}
public void setText(String text)
{
this.text = text;
}
public void setColor(RGB color)
{
this.color = color;
}
public void setAlign(Align align)
{
this.align = align;
}
@Override
public void render()
{
final double h = getRect().getHeight();
final double w = font.getWidth(text, h);
final Coord start;
switch (align) {
case LEFT:
start = getRect().getMin();
break;
case CENTER:
start = getRect().getCenterV1().sub_ip(w / 2D, 0);
break;
case RIGHT:
default:
start = getRect().getX2Y1().sub_ip(w, 0);
break;
}
font.draw(text, start, h, color);
}
}
@@ -0,0 +1,52 @@
package mightypork.gamecore.gui.screens;
import java.util.Collection;
import java.util.LinkedList;
import mightypork.gamecore.AppAccess;
public abstract class LayeredScreen extends Screen {
private final Collection<ScreenLayer> layers = new LinkedList<ScreenLayer>();
public LayeredScreen(AppAccess app) {
super(app);
}
@Override
protected final void renderScreen()
{
for (final ScreenLayer layer : layers) {
layer.render();
}
}
/**
* Add a layer to the screen.
*
* @param layer
*/
protected final void addLayer(ScreenLayer layer)
{
this.layers.add(layer); // will be rendered from last to first
addChildClient(layer); // connect to bus
}
/**
* Remove a layer
*
* @param layer
*/
protected final void removeLayer(ScreenLayer layer)
{
this.layers.remove(layer);
removeChildClient(layer);
}
}
@@ -0,0 +1,180 @@
package mightypork.gamecore.gui.screens;
import static org.lwjgl.opengl.GL11.*;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.control.bus.events.ScreenChangeEvent;
import mightypork.gamecore.control.interf.Destroyable;
import mightypork.gamecore.input.KeyBinder;
import mightypork.gamecore.input.KeyBindingPool;
import mightypork.gamecore.input.KeyStroke;
import mightypork.gamecore.render.Renderable;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Coord;
import mightypork.utils.math.coord.Rect;
/**
* Screen class.
*
* @author MightyPork
*/
public abstract class Screen extends Subsystem implements Renderable, Destroyable, KeyBinder, RectConstraint, ScreenChangeEvent.Listener {
private final KeyBindingPool keybindings = new KeyBindingPool();
private boolean active;
private boolean needSetupViewport = false;
public Screen(AppAccess app) {
super(app);
// disable events initially
setListening(false);
addChildClient(keybindings);
}
@Override
public final void bindKeyStroke(KeyStroke stroke, Runnable task)
{
keybindings.bindKeyStroke(stroke, task);
}
@Override
public final void unbindKeyStroke(KeyStroke stroke)
{
keybindings.unbindKeyStroke(stroke);
}
@Override
public final void deinit()
{
deinitScreen();
}
/**
* Prepare for being shown
*
* @param shown true to show, false to hide
*/
public final void setActive(boolean shown)
{
if (shown) {
active = true;
needSetupViewport = true;
onSizeChanged(getRect().getSize());
onScreenEnter();
// enable events
setListening(true);
} else {
onScreenLeave();
active = false;
// disable events
setListening(false);
}
}
/**
* Clean up before screen is destroyed.
*/
protected abstract void deinitScreen();
/**
* Called when the screen becomes active
*/
protected abstract void onScreenEnter();
/**
* Called when the screen is no longer active
*/
protected abstract void onScreenLeave();
/**
* Update GUI for new screen size
*
* @param size screen size
*/
protected void onSizeChanged(Coord size)
{
// no impl
}
/**
* Render screen contents (context is ready for 2D rendering)
*/
protected abstract void renderScreen();
/**
* @return true if screen is the curretn screen
*/
public final boolean isActive()
{
return active;
}
@Override
public final void receive(ScreenChangeEvent event)
{
if (!isActive()) return;
onSizeChanged(event.getScreenSize());
needSetupViewport = true;
}
@Override
public final Rect getRect()
{
return disp().getRect();
}
@Override
public final void render()
{
if (!isActive()) return;
if (needSetupViewport) {
setupViewport();
}
renderScreen();
}
protected void setupViewport()
{
// fix projection for changed size
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
final Coord s = disp().getSize();
glViewport(0, 0, s.xi(), s.yi());
glOrtho(0, s.x, s.y, 0, -1000, 1000);
// back to modelview
glMatrixMode(GL_MODELVIEW);
}
public abstract String getId();
}
@@ -0,0 +1,66 @@
package mightypork.gamecore.gui.screens;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.input.KeyBinder;
import mightypork.gamecore.input.KeyBindingPool;
import mightypork.gamecore.input.KeyStroke;
import mightypork.gamecore.render.Renderable;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Rect;
/**
* Screen display layer
*
* @author MightyPork
*/
public abstract class ScreenLayer extends Subsystem implements Renderable, RectConstraint, KeyBinder {
private final Screen screen;
private final KeyBindingPool keybindings = new KeyBindingPool();
public ScreenLayer(Screen screen) {
super(screen); // screen as AppAccess
this.screen = screen;
addChildClient(keybindings);
}
@Override
public final void bindKeyStroke(KeyStroke stroke, Runnable task)
{
keybindings.bindKeyStroke(stroke, task);
}
@Override
public final void unbindKeyStroke(KeyStroke stroke)
{
keybindings.unbindKeyStroke(stroke);
}
protected Screen screen()
{
return screen;
}
@Override
public Rect getRect()
{
return screen.getRect();
}
@Override
protected void deinit()
{
// noimpl
}
}
@@ -0,0 +1,60 @@
package mightypork.gamecore.gui.screens;
import java.util.HashMap;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.control.bus.events.ScreenRequestEvent;
import mightypork.gamecore.render.Renderable;
import mightypork.utils.logging.Log;
public class ScreenRegistry extends Subsystem implements ScreenRequestEvent.Listener, Renderable {
private final HashMap<String, Screen> screens = new HashMap<String, Screen>();
private Screen active = null;
public ScreenRegistry(AppAccess app) {
super(app);
}
public void add(Screen screen)
{
screens.put(screen.getId(), screen);
addChildClient(screen);
}
@Override
public void showScreen(String key)
{
Log.f3("Request to show screen \"" + key + "\"");
final Screen toshow = screens.get(key);
if (toshow == null) throw new RuntimeException("Screen " + key + " not defined.");
if (active != null) active.setActive(false);
toshow.setActive(true);
active = toshow;
}
@Override
public void render()
{
if (active != null) active.render();
}
@Override
protected void deinit()
{
// no impl
}
}
+37
View File
@@ -0,0 +1,37 @@
package mightypork.gamecore.input;
/**
* Triggered action
*
* @author MightyPork
*/
public abstract class Action implements Runnable {
private boolean enabled = true;
/**
* Enable the action
*
* @param enable true to enable
*/
public final void enable(boolean enable)
{
this.enabled = enable;
}
@Override
public final void run()
{
if (enabled) execute();
}
/**
* Do the work.
*/
public abstract void execute();
}
@@ -0,0 +1,7 @@
package mightypork.gamecore.input;
public interface ActionTrigger {
void setAction(Action action);
}
@@ -0,0 +1,207 @@
package mightypork.gamecore.input;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.control.bus.events.KeyboardEvent;
import mightypork.gamecore.control.bus.events.MouseButtonEvent;
import mightypork.gamecore.control.bus.events.MouseMotionEvent;
import mightypork.gamecore.control.interf.Updateable;
import mightypork.rogue.events.ActionRequest;
import mightypork.rogue.events.ActionRequest.RequestType;
import mightypork.utils.math.constraints.NumberConstraint;
import mightypork.utils.math.coord.Coord;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
public class InputSystem extends Subsystem implements Updateable, KeyBinder {
// listeners
private final KeyBindingPool keybindings;
private boolean yAxisDown = true;
private static boolean inited = false;
public InputSystem(AppAccess app) {
super(app);
initDevices();
// global keybindings
keybindings = new KeyBindingPool();
addChildClient(keybindings);
}
@Override
public final void deinit()
{
Mouse.destroy();
Keyboard.destroy();
}
private static void initDevices()
{
if (inited) return;
inited = true;
try {
Mouse.create();
Keyboard.create();
Keyboard.enableRepeatEvents(false);
} catch (final LWJGLException e) {
throw new RuntimeException("Failed to initialize input devices.", e);
}
}
@Override
public final void bindKeyStroke(KeyStroke stroke, Runnable task)
{
keybindings.bindKeyStroke(stroke, task);
}
@Override
public void unbindKeyStroke(KeyStroke stroke)
{
keybindings.unbindKeyStroke(stroke);
}
@Override
public void update(double delta)
{
// was destroyed
if (!Display.isCreated()) return;
if (!Mouse.isCreated()) return;
if (!Keyboard.isCreated()) return;
Display.processMessages();
final Coord moveSum = Coord.zero();
final Coord lastPos = Coord.zero();
boolean wasMouse = false;
while (Mouse.next()) {
onMouseEvent(moveSum, lastPos);
wasMouse = true;
}
if (wasMouse && !moveSum.isZero()) bus().send(new MouseMotionEvent(lastPos, moveSum));
while (Keyboard.next()) {
onKeyEvent();
}
if (Display.isCloseRequested()) {
bus().send(new ActionRequest(RequestType.SHUTDOWN));
}
}
private void onMouseEvent(Coord moveSum, Coord lastPos)
{
final int button = Mouse.getEventButton();
final boolean down = Mouse.getEventButtonState();
final Coord pos = new Coord(Mouse.getEventX(), Mouse.getEventY());
final Coord move = new Coord(Mouse.getEventDX(), Mouse.getEventDY());
final int wheeld = Mouse.getEventDWheel();
if (yAxisDown) {
flipScrY(pos);
move.mul_ip(1, -1, 1);
}
if (button != -1 || wheeld != 0) {
bus().send(new MouseButtonEvent(pos, button, down, wheeld));
}
moveSum.add_ip(move);
lastPos.setTo(pos);
}
private void onKeyEvent()
{
final int key = Keyboard.getEventKey();
final boolean down = Keyboard.getEventKeyState();
final char c = Keyboard.getEventCharacter();
bus().send(new KeyboardEvent(key, c, down));
}
private void flipScrY(Coord c)
{
if (disp() != null) {
c.setY_ip(disp().getSize().y - c.y);
}
}
/**
* Set whether Y axis should go top-down instead of LWJGL default bottom-up.<br>
* Default = true.
*
* @param yAxisDown
*/
public void setYDown(boolean yAxisDown)
{
this.yAxisDown = yAxisDown;
}
/**
* Get absolute mouse position
*
* @return mouse position
*/
public Coord getMousePos()
{
final Coord pos = new Coord(Mouse.getX(), Mouse.getY());
flipScrY(pos);
return pos;
}
public void grabMouse(boolean grab)
{
Mouse.setGrabbed(grab);
}
private final NumberConstraint cmousex = new NumberConstraint() {
@Override
public double getValue()
{
return getMousePos().x;
}
};
private final NumberConstraint cmousey = new NumberConstraint() {
@Override
public double getValue()
{
return getMousePos().y;
}
};
public NumberConstraint c_mouse_x()
{
return cmousex;
}
public NumberConstraint c_mouse_y()
{
return cmousey;
}
}
@@ -0,0 +1,22 @@
package mightypork.gamecore.input;
public interface KeyBinder {
/**
* Bind handler to a keystroke, replace current handler if any
*
* @param stroke trigger keystroke
* @param task handler
*/
void bindKeyStroke(KeyStroke stroke, Runnable task);
/**
* Remove handler from a keystroke (id any)
*
* @param stroke stroke
*/
void unbindKeyStroke(KeyStroke stroke);
}
@@ -0,0 +1,66 @@
package mightypork.gamecore.input;
import mightypork.gamecore.control.bus.events.KeyboardEvent;
/**
* Key binding, trigger activated by a keystroke event
*
* @author MightyPork
*/
public class KeyBinding implements KeyboardEvent.Listener {
private final KeyStroke keystroke;
private Runnable handler;
private boolean wasActive = false;
/**
* @param stroke trigger keystroke
* @param handler action
*/
public KeyBinding(KeyStroke stroke, Runnable handler) {
this.keystroke = stroke;
this.handler = handler;
wasActive = keystroke.isActive();
}
/**
* Check for equality of keystroke
*
* @param stroke other keystroke
* @return true if keystrokes are equal (cannot co-exist)
*/
public boolean matches(KeyStroke stroke)
{
return this.keystroke.equals(stroke);
}
/**
* @param handler event handler
*/
public void setHandler(Runnable handler)
{
this.handler = handler;
}
@Override
public void receive(KeyboardEvent event)
{
// ignore unrelated events
if (!keystroke.getKeys().contains(event.getKey())) return;
// run handler when event was met
if (keystroke.isActive() && !wasActive) {
handler.run();
}
wasActive = keystroke.isActive();
}
}
@@ -0,0 +1,70 @@
package mightypork.gamecore.input;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import mightypork.gamecore.control.bus.events.KeyboardEvent;
import mightypork.utils.logging.Log;
/**
* Key binding pool
*
* @author MightyPork
*/
public class KeyBindingPool implements KeyBinder, KeyboardEvent.Listener {
private final Set<KeyBinding> bindings = new HashSet<KeyBinding>();
/**
* Bind handler to a keystroke, replace current handler if any
*
* @param stroke trigger keystroke
* @param task handler
*/
@Override
public void bindKeyStroke(KeyStroke stroke, Runnable task)
{
for (final KeyBinding kb : bindings) {
if (kb.matches(stroke)) {
Log.w("Duplicate KeyBinding (" + stroke + "), using newest handler.");
kb.setHandler(task);
return;
}
}
bindings.add(new KeyBinding(stroke, task));
}
/**
* Remove handler from keystroke (id any)
*
* @param stroke stroke
*/
@Override
public void unbindKeyStroke(KeyStroke stroke)
{
final Iterator<KeyBinding> iter = bindings.iterator();
while (iter.hasNext()) {
final KeyBinding kb = iter.next();
if (kb.matches(stroke)) {
iter.remove();
return;
}
}
}
@Override
public void receive(KeyboardEvent event)
{
for (final KeyBinding kb : bindings) {
kb.receive(event);
}
}
}
@@ -0,0 +1,120 @@
package mightypork.gamecore.input;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.lwjgl.input.Keyboard;
/**
* Key stroke trigger
*
* @author MightyPork
*/
public class KeyStroke {
private final Set<Integer> keys = new LinkedHashSet<Integer>();
private final boolean fallingEdge;
/**
* KeyStroke
*
* @param fallingEdge true for falling edge, up for rising edge
* @param keys keys that must be pressed
*/
public KeyStroke(boolean fallingEdge, int... keys) {
this.fallingEdge = fallingEdge;
for (final int k : keys) {
this.keys.add(k);
}
}
/**
* Rising edge keystroke
*
* @param keys
*/
public KeyStroke(int... keys) {
fallingEdge = false;
for (final int k : keys) {
this.keys.add(k);
}
}
/**
* @return true if the keystroke is currently satisfied (keys pressed)
*/
public boolean isActive()
{
boolean st = true;
for (final int k : keys) {
st &= Keyboard.isKeyDown(k);
}
return fallingEdge ? st : !st;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((keys == null) ? 0 : keys.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof KeyStroke)) return false;
final KeyStroke other = (KeyStroke) obj;
if (keys == null) {
if (other.keys != null) return false;
} else if (!keys.equals(other.keys)) {
return false;
}
if (fallingEdge != other.fallingEdge) return false;
return true;
}
@Override
public String toString()
{
String s = "(";
int cnt = 0;
final Iterator<Integer> i = keys.iterator();
for (; i.hasNext(); cnt++) {
if (cnt > 0) s += "+";
s += Keyboard.getKeyName(i.next());
}
s += fallingEdge ? ",DOWN" : ",UP";
s += ")";
return s;
}
/**
* @return the key set
*/
public Set<Integer> getKeys()
{
return keys;
}
}
+140
View File
@@ -0,0 +1,140 @@
package mightypork.gamecore.input;
import org.lwjgl.input.Keyboard;
/**
* Key constants, from LWJGL {@link Keyboard}
*
* @author MightyPork
*/
public interface Keys {
//@formatter:off
public static final int CHAR_NONE = '\0';
public static final int KEY_NONE = 0x00;
public static final int KEY_ESCAPE = 0x01;
public static final int KEY_1 = 0x02;
public static final int KEY_2 = 0x03;
public static final int KEY_3 = 0x04;
public static final int KEY_4 = 0x05;
public static final int KEY_5 = 0x06;
public static final int KEY_6 = 0x07;
public static final int KEY_7 = 0x08;
public static final int KEY_8 = 0x09;
public static final int KEY_9 = 0x0A;
public static final int KEY_0 = 0x0B;
public static final int KEY_Q = 0x10;
public static final int KEY_W = 0x11;
public static final int KEY_E = 0x12;
public static final int KEY_R = 0x13;
public static final int KEY_T = 0x14;
public static final int KEY_Y = 0x15;
public static final int KEY_U = 0x16;
public static final int KEY_I = 0x17;
public static final int KEY_O = 0x18;
public static final int KEY_P = 0x19;
public static final int KEY_A = 0x1E;
public static final int KEY_S = 0x1F;
public static final int KEY_D = 0x20;
public static final int KEY_F = 0x21;
public static final int KEY_G = 0x22;
public static final int KEY_H = 0x23;
public static final int KEY_J = 0x24;
public static final int KEY_K = 0x25;
public static final int KEY_L = 0x26;
public static final int KEY_Z = 0x2C;
public static final int KEY_X = 0x2D;
public static final int KEY_C = 0x2E;
public static final int KEY_V = 0x2F;
public static final int KEY_B = 0x30;
public static final int KEY_N = 0x31;
public static final int KEY_M = 0x32;
public static final int KEY_MINUS = 0x0C;
public static final int KEY_EQUALS = 0x0D;
public static final int KEY_SLASH = 0x35;
public static final int KEY_BACKSLASH = 0x2B;
public static final int KEY_LBRACKET = 0x1A;
public static final int KEY_RBRACKET = 0x1B;
public static final int KEY_SEMICOLON = 0x27;
public static final int KEY_APOSTROPHE = 0x28;
public static final int KEY_GRAVE = 0x29;
public static final int KEY_COMMA = 0x33;
public static final int KEY_PERIOD = 0x34;
public static final int KEY_SPACE = 0x39;
public static final int KEY_BACKSPACE = 0x0E;
public static final int KEY_TAB = 0x0F;
public static final int KEY_F1 = 0x3B;
public static final int KEY_F2 = 0x3C;
public static final int KEY_F3 = 0x3D;
public static final int KEY_F4 = 0x3E;
public static final int KEY_F5 = 0x3F;
public static final int KEY_F6 = 0x40;
public static final int KEY_F7 = 0x41;
public static final int KEY_F8 = 0x42;
public static final int KEY_F9 = 0x43;
public static final int KEY_F10 = 0x44;
public static final int KEY_F11 = 0x57;
public static final int KEY_F12 = 0x58;
public static final int KEY_F13 = 0x64;
public static final int KEY_F14 = 0x65;
public static final int KEY_F15 = 0x66;
public static final int KEY_CAPSLOCK = 0x3A;
public static final int KEY_SCROLLLOCK = 0x46;
public static final int KEY_NUMLOCK = 0x45;
public static final int KEY_SUBTRACT = 0x4A; /* - on numeric keypad */
public static final int KEY_ADD = 0x4E; /* + on numeric keypad */
public static final int KEY_NUMPAD0 = 0x52;
public static final int KEY_NUMPAD1 = 0x4F;
public static final int KEY_NUMPAD2 = 0x50;
public static final int KEY_NUMPAD3 = 0x51;
public static final int KEY_NUMPAD4 = 0x4B;
public static final int KEY_NUMPAD5 = 0x4C;
public static final int KEY_NUMPAD6 = 0x4D;
public static final int KEY_NUMPAD7 = 0x47;
public static final int KEY_NUMPAD8 = 0x48;
public static final int KEY_NUMPAD9 = 0x49;
public static final int KEY_DECIMAL = 0x53; /* . on numeric keypad */
public static final int KEY_NUMPADENTER = 0x9C; /* Enter on numeric keypad */
public static final int KEY_DIVIDE = 0xB5; /* / on numeric keypad */
public static final int KEY_MULTIPLY = 0x37; /* * on numeric keypad */
public static final int KEY_LCONTROL = 0x1D;
public static final int KEY_RCONTROL = 0x9D;
public static final int KEY_LALT = 0x38;
public static final int KEY_RALT = 0xB8;
public static final int KEY_LSHIFT = 0x2A;
public static final int KEY_RSHIFT = 0x36;
public static final int KEY_LMETA = 0xDB;
public static final int KEY_RMETA = 0xDC;
public static final int KEY_UP = 0xC8; /* UpArrow on arrow keypad */
public static final int KEY_DOWN = 0xD0; /* DownArrow on arrow keypad */
public static final int KEY_LEFT = 0xCB; /* LeftArrow on arrow keypad */
public static final int KEY_RIGHT = 0xCD; /* RightArrow on arrow keypad */
public static final int KEY_HOME = 0xC7; /* Home on arrow keypad */
public static final int KEY_END = 0xCF; /* End on arrow keypad */
public static final int KEY_PAGEUP = 0xC9; /* PgUp on arrow keypad */
public static final int KEY_PAGEDOWN = 0xD1; /* PgDn on arrow keypad */
public static final int KEY_RETURN = 0x1C;
public static final int KEY_PAUSE = 0xC5; /* Pause */
public static final int KEY_INSERT = 0xD2; /* Insert on arrow keypad */
public static final int KEY_DELETE = 0xD3; /* Delete on arrow keypad */
//@formatter:on
}
@@ -0,0 +1,108 @@
package mightypork.gamecore.loading;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import mightypork.gamecore.BusAccess;
import mightypork.gamecore.control.bus.events.MainLoopTaskRequest;
import mightypork.gamecore.control.bus.events.ResourceLoadRequest;
import mightypork.gamecore.control.interf.Destroyable;
import mightypork.utils.logging.Log;
/**
* Asynchronous resource loading thread.
*
* @author MightyPork
*/
public class AsyncResourceLoader extends Thread implements ResourceLoadRequest.Listener, Destroyable {
public static void launch(BusAccess app)
{
(new AsyncResourceLoader(app)).start();
}
private final ExecutorService exs = Executors.newCachedThreadPool();
private final LinkedBlockingQueue<DeferredResource> toLoad = new LinkedBlockingQueue<DeferredResource>();
private boolean stopped;
private final BusAccess app;
public AsyncResourceLoader(BusAccess app) {
super("Deferred loader");
this.app = app;
app.bus().subscribe(this);
}
@Override
public void loadResource(DeferredResource resource)
{
toLoad.add(resource);
}
@Override
public void run()
{
Log.f3("Asynchronous resource loader started.");
while (!stopped) {
try {
final DeferredResource def = toLoad.take();
if (def == null) continue;
if (!def.isLoaded()) {
// skip nulls
if (def instanceof NullResource) continue;
// textures & fonts needs to be loaded in main thread
if (def.getClass().isAnnotationPresent(MustLoadInMainThread.class)) {
Log.f3("<LOADER> Delegating to main thread:\n " + Log.str(def));
app.bus().send(new MainLoopTaskRequest(new Runnable() {
@Override
public void run()
{
def.load();
}
}));
continue;
}
Log.f3("<LOADER> Loading async:\n " + Log.str(def));
exs.submit(new Runnable() {
@Override
public void run()
{
def.load();
}
});
}
} catch (final InterruptedException ignored) {
//
}
}
}
@Override
public void destroy()
{
stopped = true;
exs.shutdownNow();
}
}
@@ -0,0 +1,135 @@
package mightypork.gamecore.loading;
import mightypork.gamecore.control.interf.Destroyable;
import mightypork.utils.logging.Log;
import mightypork.utils.logging.LoggedName;
/**
* Deferred resource abstraction.<br>
* Resources implementing {@link NullResource} will be treated as fake and not
* attempted to load.
*
* @author MightyPork
*/
@LoggedName(name = "Resource")
public abstract class BaseDeferredResource implements DeferredResource, Destroyable {
private final String resource;
private boolean loadFailed = false;
private boolean loadAttempted = false;
public BaseDeferredResource(String resource) {
this.resource = resource;
}
@Override
public synchronized final void load()
{
if (loadAttempted) {
Log.w("<RES> Already loaded @ load():\n " + this);
return;
}
loadAttempted = true;
loadFailed = false;
if (isNull()) return;
try {
if (resource == null) {
throw new NullPointerException("Resource string cannot be null for non-null resource.");
}
Log.f3("<RES> Loading:\n " + this);
loadResource(resource);
Log.f3("<RES> Loaded:\n " + this);
} catch (final Exception e) {
loadFailed = true;
Log.e("<RES> Failed to load:\n " + this, e);
}
}
@Override
public final boolean isLoaded()
{
if (isNull()) return false;
return loadAttempted && !loadFailed;
}
/**
* Check if the resource is loaded; if not, try to do so.
*
* @return true if it's loaded now.
*/
public synchronized final boolean ensureLoaded()
{
if (isNull()) return false;
if (isLoaded()) {
return true;
} else {
Log.w("<RES> First use, not loaded yet - loading directly\n " + this);
load();
}
return isLoaded();
}
/**
* Load the resource. Called from load() - once only.
*
* @param resource the path / name of a resource
* @throws Exception when some problem prevented the resource from being
* loaded.
*/
protected abstract void loadResource(String resource) throws Exception;
@Override
public abstract void destroy();
@Override
public String toString()
{
return Log.str(getClass()) + "(\"" + resource + "\")";
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((resource == null) ? 0 : resource.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj) return true;
if (obj == null) return false;
if (!(obj instanceof BaseDeferredResource)) return false;
final BaseDeferredResource other = (BaseDeferredResource) obj;
if (resource == null) {
if (other.resource != null) return false;
} else if (!resource.equals(other.resource)) return false;
return true;
}
private boolean isNull()
{
return this instanceof NullResource;
}
}
@@ -0,0 +1,23 @@
package mightypork.gamecore.loading;
/**
* Deferred resource
*
* @author MightyPork
*/
public interface DeferredResource {
/**
* Load the actual resource, if not loaded yet.
*/
void load();
/**
* Check if resource was successfully loaded.
*
* @return true if already loaded
*/
boolean isLoaded();
}
@@ -0,0 +1,18 @@
package mightypork.gamecore.loading;
import java.lang.annotation.*;
/**
* Resource that is texture-based and therefore needs to be loaded in the main
* thread (ie. main loop).
*
* @author MightyPork
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface MustLoadInMainThread {}
@@ -0,0 +1,12 @@
package mightypork.gamecore.loading;
/**
* Resource that is used as a placeholder instead of an actual resource; this
* resource should not be attempted to be loaded.
*
* @author MightyPork
*/
public interface NullResource {
}
@@ -0,0 +1,241 @@
package mightypork.gamecore.render;
import static org.lwjgl.opengl.GL11.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.imageio.ImageIO;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.control.bus.events.ScreenChangeEvent;
import mightypork.utils.logging.Log;
import mightypork.utils.math.constraints.RectConstraint;
import mightypork.utils.math.coord.Coord;
import mightypork.utils.math.coord.Rect;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
public class DisplaySystem extends Subsystem implements RectConstraint {
private DisplayMode windowDisplayMode;
private int targetFps;
public DisplaySystem(AppAccess app) {
super(app);
}
@Override
protected void deinit()
{
Display.destroy();
}
public void setTargetFps(int fps)
{
this.targetFps = fps;
}
public void createMainWindow(int width, int height, boolean resizable, boolean fullscreen, String title)
{
try {
Display.setDisplayMode(windowDisplayMode = new DisplayMode(width, height));
Display.setResizable(resizable);
Display.setVSyncEnabled(true);
Display.setTitle(title);
Display.create();
if (fullscreen) {
switchFullscreen();
Display.update();
}
Render.init();
} catch (final LWJGLException e) {
throw new RuntimeException("Could not initialize screen", e);
}
}
/**
* Toggle FS if possible
*/
public void switchFullscreen()
{
try {
if (!Display.isFullscreen()) {
Log.f3("Entering fullscreen.");
// save window resize
windowDisplayMode = new DisplayMode(Display.getWidth(), Display.getHeight());
Display.setDisplayMode(Display.getDesktopDisplayMode());
Display.setFullscreen(true);
Display.update();
} else {
Log.f3("Leaving fullscreen.");
Display.setDisplayMode(windowDisplayMode);
Display.update();
}
bus().send(new ScreenChangeEvent(true, Display.isFullscreen(), getSize()));
} catch (final Throwable t) {
Log.e("Failed to toggle fullscreen mode.", t);
try {
Display.setDisplayMode(windowDisplayMode);
Display.update();
} catch (final Throwable t1) {
throw new RuntimeException("Failed to revert failed fullscreen toggle.", t1);
}
}
}
public Screenshot takeScreenshot()
{
glReadBuffer(GL_FRONT);
final int width = Display.getDisplayMode().getWidth();
final int height = Display.getDisplayMode().getHeight();
final int bpp = 4; // Assuming a 32-bit display with a byte each for red,
// green, blue, and alpha.
final ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * bpp);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
final Screenshot sc = new Screenshot(width, height, bpp, buffer);
return sc;
}
/**
* @return true if close was requested (i.e. click on cross)
*/
public boolean isCloseRequested()
{
return Display.isCloseRequested();
}
/**
* Get fullscreen state
*
* @return is fullscreen
*/
public boolean isFullscreen()
{
return Display.isFullscreen();
}
/**
* Get screen size
*
* @return size
*/
public Coord getSize()
{
return new Coord(getWidth(), getHeight());
}
public int getWidth()
{
return Display.getWidth();
}
public int getHeight()
{
return Display.getHeight();
}
/**
* Start a OpenGL frame
*/
public void beginFrame()
{
// handle resize
if (Display.wasResized()) {
bus().send(new ScreenChangeEvent(false, Display.isFullscreen(), getSize()));
}
glLoadIdentity();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
/**
* End an OpenGL frame, flip buffers, sync to fps.
*/
public void endFrame()
{
Display.update(false); // don't poll input devices
Display.sync(targetFps);
}
@Override
public Rect getRect()
{
return new Rect(getSize());
}
public static class Screenshot {
private int width;
private int height;
private int bpp;
private ByteBuffer bytes;
private BufferedImage image;
public Screenshot(int width, int height, int bpp, ByteBuffer buffer) {
this.width = width;
this.height = height;
this.bpp = bpp;
this.bytes = buffer;
}
public BufferedImage getImage()
{
if (image != null) return image;
image = new BufferedImage(this.width, this.height, BufferedImage.TYPE_INT_RGB);
// convert to a buffered image
for (int x = 0; x < this.width; x++) {
for (int y = 0; y < this.height; y++) {
final int i = (x + (this.width * y)) * this.bpp;
final int r = this.bytes.get(i) & 0xFF;
final int g = this.bytes.get(i + 1) & 0xFF;
final int b = this.bytes.get(i + 2) & 0xFF;
image.setRGB(x, this.height - (y + 1), (0xFF << 24) | (r << 16) | (g << 8) | b);
}
}
return image;
}
public void save(File file) throws IOException
{
ImageIO.write(getImage(), "PNG", file);
}
}
}
+494
View File
@@ -0,0 +1,494 @@
package mightypork.gamecore.render;
import static org.lwjgl.opengl.GL11.*;
import java.io.IOException;
import mightypork.gamecore.resources.textures.TxQuad;
import mightypork.utils.files.FileUtils;
import mightypork.utils.logging.Log;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
import mightypork.utils.math.coord.Rect;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.opengl.Texture;
import org.newdawn.slick.opengl.TextureImpl;
import org.newdawn.slick.opengl.TextureLoader;
import org.newdawn.slick.util.ResourceLoader;
/**
* Render utilities
*
* @author MightyPork
*/
public class Render {
private static final Coord AXIS_X = new Coord(1, 0, 0);
private static final Coord AXIS_Y = new Coord(0, 1, 0);
private static final Coord AXIS_Z = new Coord(0, 0, 1);
public static void init()
{
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glDisable(GL_LIGHTING);
glClearDepth(1f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glEnable(GL_NORMALIZE);
glShadeModel(GL_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
/**
* Bind GL color
*
* @param color RGB color
*/
public static void setColor(RGB color)
{
if (color != null) glColor4d(color.r, color.g, color.b, color.a);
}
/**
* Bind GL color
*
* @param color RGB color
* @param alpha alpha multiplier
*/
public static void setColor(RGB color, double alpha)
{
if (color != null) glColor4d(color.r, color.g, color.b, color.a * alpha);
}
/**
* Translate with coord
*
* @param coord coord
*/
public static void translate(Coord coord)
{
glTranslated(coord.x, coord.y, coord.z);
}
/**
* Scale
*
* @param factor vector of scaling factors
*/
public static void scale(Coord factor)
{
glScaled(factor.x, factor.y, factor.z);
}
/**
* Scale by X factor
*
* @param factor scaling factor
*/
public static void scaleXY(double factor)
{
glScaled(factor, factor, 1);
}
/**
* Scale by X factor
*
* @param factor scaling factor
*/
public static void scaleX(double factor)
{
glScaled(factor, 1, 1);
}
/**
* Scale by Y factor
*
* @param factor scaling factor
*/
public static void scaleY(double factor)
{
glScaled(1, factor, 1);
}
/**
* Scale by Z factor
*
* @param factor scaling factor
*/
public static void scaleZ(double factor)
{
glScaled(1, 1, factor);
}
/**
* Rotate around X axis
*
* @param angle deg
*/
public static void rotateX(double angle)
{
rotate(angle, AXIS_X);
}
/**
* Rotate around Y axis
*
* @param angle deg
*/
public static void rotateY(double angle)
{
rotate(angle, AXIS_Y);
}
/**
* Rotate around Z axis
*
* @param angle deg
*/
public static void rotateZ(double angle)
{
rotate(angle, AXIS_Z);
}
/**
* Rotate
*
* @param angle rotate angle
* @param axis rotation axis
*/
public static void rotate(double angle, Coord axis)
{
final Coord vec = axis.norm(1);
glRotated(angle, vec.x, vec.y, vec.z);
}
private static int pushed = 0;
/**
* Store GL state
*/
public static void pushState()
{
pushed++;
if (pushed >= 3) {
Log.w("Suspicious amount of state pushes: " + pushed);
}
// Log.f3("push : "+pushed);
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glPushClientAttrib(GL11.GL_ALL_CLIENT_ATTRIB_BITS);
GL11.glPushMatrix();
}
/**
* Restore Gl state
*/
public static void popState()
{
if (pushed == 0) {
Log.w("Pop without push.");
}
pushed--;
// Log.f3("pop : "+pushed);
GL11.glPopMatrix();
GL11.glPopClientAttrib();
GL11.glPopAttrib();
}
/**
* Load texture
*
* @param resourcePath
* @return the loaded texture
*/
public synchronized static Texture loadTexture(String resourcePath)
{
try {
final String ext = FileUtils.getExtension(resourcePath).toUpperCase();
final Texture texture = TextureLoader.getTexture(ext, ResourceLoader.getResourceAsStream(resourcePath));
if (texture == null) {
Log.w("Texture " + resourcePath + " could not be loaded.");
}
return texture;
} catch (final IOException e) {
Log.e("Loading of texture " + resourcePath + " failed.", e);
throw new RuntimeException("Could not load texture " + resourcePath + ".", e);
}
}
/**
* Bind texture
*
* @param texture the texture
* @param linear use linear interpolation for scaling
* @throws RuntimeException if not loaded yet
*/
private static void bindTexture(Texture texture, boolean linear) throws RuntimeException
{
texture.bind();
}
/**
* Bind texture with linear interpolation
*
* @param texture the texture
* @throws RuntimeException if not loaded yet
*/
private static void bindTexture(Texture texture) throws RuntimeException
{
bindTexture(texture, false);
}
/**
* Unbind all
*/
private static void unbindTexture()
{
if (TextureImpl.getLastBind() != null) {
TextureImpl.bindNone();
}
}
/**
* Render quad 2D
*
* @param rect rectangle
* @param color draw color
*/
public static void quad(Rect rect, RGB color)
{
setColor(color);
quad(rect);
}
/**
* Render quad
*
* @param quad the quad to draw (px)
*/
public static void quad(Rect quad)
{
final double left = quad.x1();
final double bottom = quad.y1();
final double right = quad.x2();
final double top = quad.y2();
// draw with color
unbindTexture();
// quad
glBegin(GL_QUADS);
glVertex2d(left, top);
glVertex2d(right, top);
glVertex2d(right, bottom);
glVertex2d(left, bottom);
glEnd();
}
/**
* Render textured rect (texture must be binded already)
*
* @param quad rectangle (px)
* @param uvs texture coords (0-1)
*/
public static void quadUV(Rect quad, Rect uvs)
{
glBegin(GL_QUADS);
quadUV_nobound(quad, uvs);
glEnd();
}
/**
* Draw quad without glBegin and glEnd.
*
* @param quad rectangle (px)
* @param uvs texture coords (0-1)
*/
public static void quadUV_nobound(Rect quad, Rect uvs)
{
final double left = quad.x1();
final double bottom = quad.y1();
final double right = quad.x2();
final double top = quad.y2();
final double tleft = uvs.x1();
final double tbottom = uvs.y1();
final double tright = uvs.x2();
final double ttop = uvs.y2();
// quad with texture
glTexCoord2d(tleft, ttop);
glVertex2d(left, top);
glTexCoord2d(tright, ttop);
glVertex2d(right, top);
glTexCoord2d(tright, tbottom);
glVertex2d(right, bottom);
glTexCoord2d(tleft, tbottom);
glVertex2d(left, bottom);
}
public static void quadGradH(Rect quad, RGB colorLeft, RGB colorRight)
{
final double left = quad.x1();
final double bottom = quad.y1();
final double right = quad.y2();
final double top = quad.y2();
// draw with color
unbindTexture();
glBegin(GL_QUADS);
setColor(colorLeft);
glVertex2d(left, top);
setColor(colorRight);
glVertex2d(right, top);
setColor(colorRight);
glVertex2d(right, bottom);
setColor(colorLeft);
glVertex2d(left, bottom);
glEnd();
}
public static void quadGradV(Rect quad, RGB colorTop, RGB colorBottom)
{
final double left = quad.x1();
final double bottom = quad.y1();
final double right = quad.y2();
final double top = quad.y2();
// draw with color
unbindTexture();
glBegin(GL_QUADS);
setColor(colorTop);
glVertex2d(left, top);
glVertex2d(right, top);
setColor(colorBottom);
glVertex2d(right, bottom);
glVertex2d(left, bottom);
glEnd();
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param uvs texture coords rectangle (0-1)
* @param texture texture instance
* @param tint color tint
*/
public static void quadTextured(Rect quad, Rect uvs, Texture texture, RGB tint)
{
pushState();
bindTexture(texture);
setColor(tint);
quadUV(quad, uvs);
popState();
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param uvs texture coords rectangle (px)
* @param texture texture instance
*/
public static void quadTextured(Rect quad, Rect uvs, Texture texture)
{
quadTextured(quad, uvs, texture, RGB.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param texture texture instance
*/
public static void quadTextured(Rect quad, Texture texture)
{
quadTextured(quad, Rect.one(), texture, RGB.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param txquad texture quad
*/
public static void quadTextured(Rect quad, TxQuad txquad)
{
quadTextured(quad, txquad, RGB.WHITE);
}
/**
* Render textured rect
*
* @param quad rectangle (px)
* @param txquad texture instance
* @param tint color tint
*/
public static void quadTextured(Rect quad, TxQuad txquad, RGB tint)
{
quadTextured(quad, txquad.uvs, txquad.tx, tint);
}
}
@@ -0,0 +1,16 @@
package mightypork.gamecore.render;
/**
* Can be rendered
*
* @author MightyPork
*/
public interface Renderable {
/**
* Render on screen
*/
void render();
}
@@ -0,0 +1,188 @@
package mightypork.gamecore.resources.fonts;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import java.io.InputStream;
import mightypork.gamecore.loading.BaseDeferredResource;
import mightypork.gamecore.loading.MustLoadInMainThread;
import mightypork.utils.files.FileUtils;
import mightypork.utils.logging.LoggedName;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
/**
* Font obtained from a resource file (TTF).
*
* @author MightyPork
*/
@MustLoadInMainThread
@LoggedName(name = "Font")
public class DeferredFont extends BaseDeferredResource implements GLFont {
public static enum FontStyle
{
PLAIN(Font.PLAIN), BOLD(Font.BOLD), ITALIC(Font.ITALIC), BOLD_ITALIC(Font.BOLD + Font.ITALIC);
int numeric;
private FontStyle(int style) {
this.numeric = style;
}
}
private SlickFont font = null;
private final double size;
private final FontStyle style;
private final boolean antiAlias;
private final String extraChars;
/**
* A font from resource
*
* @param resourcePath resource to load
* @param extraChars extra chars (0-255 loaded by default)
* @param size size (px)
*/
public DeferredFont(String resourcePath, String extraChars, double size) {
this(resourcePath, extraChars, size, FontStyle.PLAIN, true);
}
/**
* A font from resource
*
* @param resourcePath resource to load
* @param extraChars extra chars (0-255 loaded by default)
* @param size size (pt)
* @param style font style
*/
public DeferredFont(String resourcePath, String extraChars, double size, FontStyle style) {
this(resourcePath, extraChars, size, style, true);
}
/**
* A font from resource
*
* @param resourcePath resource to load
* @param extraChars extra chars (0-255 loaded by default)
* @param size size (pt)
* @param style font style
* @param antialias use antialiasing
*/
public DeferredFont(String resourcePath, String extraChars, double size, FontStyle style, boolean antialias) {
super(resourcePath);
this.size = size;
this.style = style;
this.antiAlias = antialias;
this.extraChars = extraChars;
}
@Override
protected final void loadResource(String path) throws FontFormatException, IOException
{
final Font awtFont = getAwtFont(path, (float) size, style.numeric);
font = new SlickFont(awtFont, antiAlias, extraChars);
}
/**
* Get a font for a resource path / name
*
* @param resource resource to load
* @param size font size (pt)
* @param style font style
* @return the {@link Font}
* @throws FontFormatException
* @throws IOException
*/
protected Font getAwtFont(String resource, float size, int style) throws FontFormatException, IOException
{
InputStream in = null;
try {
in = FileUtils.getResource(resource);
Font awtFont = Font.createFont(Font.TRUETYPE_FONT, in);
awtFont = awtFont.deriveFont(size);
awtFont = awtFont.deriveFont(style);
return awtFont;
} finally {
try {
if (in != null) in.close();
} catch (final IOException e) {
//pass
}
}
}
/**
* Draw string
*
* @param str string to draw
* @param color draw color
*/
@Override
public void draw(String str, RGB color)
{
if (!ensureLoaded()) return;
font.draw(str, color);
}
/**
* Get size needed to render give string
*
* @param text string to check
* @return coord (width, height)
*/
@Override
public Coord getNeededSpace(String text)
{
if (!ensureLoaded()) return Coord.zero();
return font.getNeededSpace(text);
}
/**
* @return font height
*/
@Override
public int getHeight()
{
if (!ensureLoaded()) return 0;
return font.getHeight();
}
@Override
public int getWidth(String text)
{
return font.getWidth(text);
}
@Override
public void destroy()
{
// this will have to suffice
font = null;
}
}
@@ -0,0 +1,64 @@
package mightypork.gamecore.resources.fonts;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import mightypork.utils.logging.LoggedName;
/**
* Font obtained from the OS
*
* @author MightyPork
*/
@LoggedName(name = "FontNative")
public class DeferredFontNative extends DeferredFont {
/**
* A font from OS, found by name
*
* @param fontName font family name
* @param extraChars extra chars (0-255 loaded by default)
* @param size size (pt)
*/
public DeferredFontNative(String fontName, String extraChars, double size) {
super(fontName, extraChars, size);
}
/**
* A font from OS, found by name
*
* @param fontName font family name
* @param extraChars extra chars (0-255 loaded by default)
* @param size size (pt)
* @param style font style
*/
public DeferredFontNative(String fontName, String extraChars, double size, FontStyle style) {
super(fontName, extraChars, size, style);
}
/**
* A font from OS, found by name
*
* @param fontName font family name
* @param extraChars extra chars (0-255 loaded by default)
* @param size size (pt)
* @param style font style
* @param antialias use antialiasing
*/
public DeferredFontNative(String fontName, String extraChars, double size, FontStyle style, boolean antialias) {
super(fontName, extraChars, size, style, antialias);
}
@Override
protected Font getAwtFont(String resource, float size, int style) throws FontFormatException, IOException
{
return new Font(resource, style, (int) size);
}
}
@@ -0,0 +1,75 @@
package mightypork.gamecore.resources.fonts;
import java.util.HashMap;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.AppAdapter;
import mightypork.gamecore.control.bus.events.ResourceLoadRequest;
import mightypork.utils.logging.Log;
import org.newdawn.slick.opengl.Texture;
/**
* Font loader and registry
*
* @author MightyPork
*/
public class FontBank extends AppAdapter {
private static final GLFont NULL_FONT = new NullFont();
public FontBank(AppAccess app) {
super(app);
}
private final HashMap<String, GLFont> fonts = new HashMap<String, GLFont>();
/**
* Load a {@link DeferredFont}
*
* @param key font key
* @param font font instance
*/
public void loadFont(String key, DeferredFont font)
{
bus().send(new ResourceLoadRequest(font));
fonts.put(key, font);
}
/**
* Add a {@link GLFont} to the bank.
*
* @param key font key
* @param font font instance
*/
public void addFont(String key, GLFont font)
{
fonts.put(key, font);
}
/**
* Get a loaded {@link Texture}
*
* @param key texture key
* @return the texture
*/
public GLFont getFont(String key)
{
final GLFont f = fonts.get(key);
if (f == null) {
Log.w("There's no font called " + key + "!");
return NULL_FONT;
}
return f;
}
}
@@ -0,0 +1,58 @@
package mightypork.gamecore.resources.fonts;
import mightypork.gamecore.render.Render;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
import mightypork.utils.math.coord.Rect;
public class FontRenderer {
private final GLFont font;
public FontRenderer(GLFont font) {
if (font == null) throw new NullPointerException("Font cannot be null.");
this.font = font;
}
public void draw(String text, Coord pos, double height, RGB color)
{
Render.pushState();
Render.translate(pos.round());
Render.scaleXY(getScale(height));
font.draw(text, color);
Render.popState();
}
public Coord getNeededSpace(String text, double height)
{
return font.getNeededSpace(text).mul(getScale(height));
}
public double getWidth(String text, double height)
{
return getNeededSpace(text, height).x;
}
public Rect getBounds(String text, Coord pos, double height)
{
return Rect.fromSize(getNeededSpace(text, height)).add(pos);
}
private double getScale(double height)
{
return height / font.getHeight();
}
}
@@ -0,0 +1,40 @@
package mightypork.gamecore.resources.fonts;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
public interface GLFont {
/**
* Draw string at position
*
* @param text string to draw
* @param color draw color
*/
void draw(String text, RGB color);
/**
* Get suize needed to render give string
*
* @param text string to check
* @return coord (width, height)
*/
Coord getNeededSpace(String text);
/**
* @return font height
*/
int getHeight();
/**
* @param text texted text
* @return space needed
*/
int getWidth(String text);
}
@@ -0,0 +1,24 @@
package mightypork.gamecore.resources.fonts;
/**
* Glyph tables.
*
* @author MightyPork
*/
public class Glyphs {
public static final String space = " ";
public static final String latin = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
public static final String latin_extra = "ŒÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜŸÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿĚŠČŘŽŤŇĎŮěščřžťňďůŁłđ";
public static final String numbers = "0123456789";
public static final String punctuation = ".-,.?!:;\"'";
public static final String punctuation_extra = "()¿¡»«›‹“”‘’„…";
public static final String symbols = "[]{}#$%&§*+/<=>@\\^_|~°";
public static final String symbols_extra = "¥€£¢`ƒ†‡ˆ‰•¤¦¨ªº¹²³¬­¯±´µ¶·¸¼½¾×÷™©­®→↓←↑";
public static final String basic = space + latin + numbers + punctuation + symbols;
public static final String extra = latin_extra + punctuation_extra + symbols_extra;
public static final String all = basic + extra;
}
@@ -0,0 +1,42 @@
package mightypork.gamecore.resources.fonts;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
/**
* Null font used where real resource could not be loaded.
*
* @author MightyPork
*/
public class NullFont implements GLFont {
@Override
public void draw(String str, RGB color)
{
// nope
}
@Override
public Coord getNeededSpace(String str)
{
return Coord.zero();
}
@Override
public int getHeight()
{
return 0;
}
@Override
public int getWidth(String text)
{
return 0;
}
}
@@ -0,0 +1,106 @@
package mightypork.gamecore.resources.fonts;
import static org.lwjgl.opengl.GL11.*;
import java.awt.Font;
import mightypork.gamecore.render.Render;
import mightypork.utils.math.color.RGB;
import mightypork.utils.math.coord.Coord;
import org.newdawn.slick.Color;
import org.newdawn.slick.TrueTypeFont;
/**
* Wrapper for slick font
*
* @author MightyPork
*/
public class SlickFont implements GLFont {
private final TrueTypeFont ttf;
/**
* A font with ASCII and extra chars
*
* @param font font to load
* @param antiAlias antialiasing
* @param extraChars extra chars to load
*/
public SlickFont(Font font, boolean antiAlias, String extraChars) {
ttf = new TrueTypeFont(font, antiAlias, stripASCII(extraChars));
}
private static char[] stripASCII(String chars)
{
if (chars == null) return null;
final StringBuilder sb = new StringBuilder();
for (final char c : chars.toCharArray()) {
if (c <= 255) continue; // already included in default set
sb.append(c);
}
return sb.toString().toCharArray();
}
private static void prepareForRender()
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
/**
* Draw in color
*
* @param str string to draw
* @param color text color
*/
@Override
public void draw(String str, RGB color)
{
Render.pushState();
prepareForRender();
ttf.drawString(0, 0, str, rgbToSlickColor(color));
Render.popState();
}
private static Color rgbToSlickColor(RGB rgb)
{
return new Color((float) rgb.r, (float) rgb.g, (float) rgb.b, (float) rgb.a);
}
@Override
public Coord getNeededSpace(String text)
{
return new Coord(getWidth(text), getHeight());
}
@Override
public int getHeight()
{
return ttf.getHeight();
}
@Override
public int getWidth(String text)
{
return ttf.getWidth(text);
}
}
@@ -0,0 +1,236 @@
package mightypork.gamecore.resources.sounds;
import java.io.IOException;
import mightypork.gamecore.loading.BaseDeferredResource;
import mightypork.utils.files.FileUtils;
import mightypork.utils.logging.LoggedName;
import mightypork.utils.math.coord.Coord;
import org.newdawn.slick.openal.Audio;
import org.newdawn.slick.openal.SoundStore;
/**
* Wrapper class for slick audio
*
* @author MightyPork
*/
@LoggedName(name = "Audio")
public class DeferredAudio extends BaseDeferredResource {
private enum PlayMode
{
EFFECT, MUSIC;
}
/** Audio resource */
private Audio backingAudio = null;
// last play options
private PlayMode mode = PlayMode.EFFECT;
private double pauseLoopPosition = 0;
private boolean looping = false;
private boolean paused = false;
private double lastPlayPitch = 1;
private double lastPlayGain = 1;
/**
* Create deferred primitive audio player
*
* @param resourceName resource to load when needed
*/
public DeferredAudio(String resourceName) {
super(resourceName);
}
/**
* Pause loop (remember position and stop playing) - if was looping
*/
public void pauseLoop()
{
if (!ensureLoaded()) return;
if (isPlaying() && looping) {
pauseLoopPosition = backingAudio.getPosition();
stop();
paused = true;
}
}
/**
* Resume loop (if was paused)
*
* @return source ID
*/
public int resumeLoop()
{
if (!ensureLoaded()) return -1;
int source = -1;
if (looping && paused) {
if (mode == PlayMode.MUSIC) {
source = backingAudio.playAsMusic((float) lastPlayPitch, (float) lastPlayGain, true);
} else {
source = backingAudio.playAsSoundEffect((float) lastPlayPitch, (float) lastPlayGain, true);
}
backingAudio.setPosition((float) pauseLoopPosition);
paused = false;
}
return source;
}
@Override
protected void loadResource(String resource) throws IOException
{
final String ext = FileUtils.getExtension(resource);
if (ext.equalsIgnoreCase("ogg")) {
backingAudio = SoundStore.get().getOgg(resource);
} else if (ext.equalsIgnoreCase("wav")) {
backingAudio = SoundStore.get().getWAV(resource);
} else if (ext.equalsIgnoreCase("aif")) {
backingAudio = SoundStore.get().getAIF(resource);
} else if (ext.equalsIgnoreCase("mod")) {
backingAudio = SoundStore.get().getMOD(resource);
} else {
throw new RuntimeException("Invalid audio file extension.");
}
}
public void stop()
{
if (!isLoaded()) return;
backingAudio.stop();
paused = false;
}
public boolean isPlaying()
{
if (!isLoaded()) return false;
return backingAudio.isPlaying();
}
public boolean isPaused()
{
if (!isLoaded()) return false;
return backingAudio.isPaused();
}
/**
* Play as sound effect at listener position
*
* @param pitch pitch (1 = default)
* @param gain gain (0-1)
* @param loop looping
* @return source id
*/
public int playAsEffect(double pitch, double gain, boolean loop)
{
return playAsEffect(pitch, gain, loop, SoundSystem.getListener());
}
/**
* Play as sound effect at given X-Y position
*
* @param pitch pitch (1 = default)
* @param gain gain (0-1)
* @param loop looping
* @param x
* @param y
* @return source id
*/
public int playAsEffect(double pitch, double gain, boolean loop, double x, double y)
{
return playAsEffect(pitch, gain, loop, x, y, SoundSystem.getListener().z);
}
/**
* Play as sound effect at given position
*
* @param pitch pitch (1 = default)
* @param gain gain (0-1)
* @param loop looping
* @param x
* @param y
* @param z
* @return source id
*/
public int playAsEffect(double pitch, double gain, boolean loop, double x, double y, double z)
{
if (!ensureLoaded()) return -1;
this.lastPlayPitch = pitch;
this.lastPlayGain = gain;
looping = loop;
mode = PlayMode.EFFECT;
return backingAudio.playAsSoundEffect((float) pitch, (float) gain, loop, (float) x, (float) y, (float) z);
}
/**
* Play as sound effect at given position
*
* @param pitch pitch (1 = default)
* @param gain gain (0-1)
* @param loop looping
* @param pos coord
* @return source id
*/
public int playAsEffect(double pitch, double gain, boolean loop, Coord pos)
{
if (!ensureLoaded()) return -1;
return playAsEffect(pitch, gain, loop, pos.x, pos.y, pos.z);
}
/**
* Play as music using source 0.<br>
* Discouraged, since this does not allow cross-fading.
*
* @param pitch play pitch
* @param gain play gain
* @param loop looping
* @return source
*/
public int playAsMusic(double pitch, double gain, boolean loop)
{
if (!ensureLoaded()) return -1;
this.lastPlayPitch = (float) pitch;
this.lastPlayGain = (float) gain;
looping = loop;
mode = PlayMode.MUSIC;
return backingAudio.playAsMusic((float) pitch, (float) gain, loop);
}
@Override
public void destroy()
{
if (!isLoaded() || backingAudio == null) return;
backingAudio.release();
backingAudio = null;
}
}
@@ -0,0 +1,51 @@
package mightypork.gamecore.resources.sounds;
import mightypork.utils.math.Calc;
import mightypork.utils.objects.Mutable;
/**
* Volume combined of multiple volumes, combining them (multiplication).
*
* @author MightyPork
*/
public class JointVolume extends Mutable<Double> {
private final Mutable<Double>[] volumes;
/**
* Create joint volume with master gain of 1
*
* @param volumes individual volumes to join
*/
public JointVolume(Mutable<Double>... volumes) {
super(1D);
this.volumes = volumes;
}
/**
* Get combined gain (multiplied)
*/
@Override
public Double get()
{
double d = super.get();
for (final Mutable<Double> v : volumes)
d *= v.get();
return Calc.clampd(d, 0, 1);
}
/**
* Set master gain
*/
@Override
public void set(Double o)
{
super.set(o);
}
}
@@ -0,0 +1,20 @@
package mightypork.gamecore.resources.sounds;
import mightypork.gamecore.loading.NullResource;
import mightypork.utils.logging.LoggedName;
/**
* Placeholder for cases where no matching audio is found and
* {@link NullPointerException} has to be avoided.
*
* @author MightyPork
*/
@LoggedName(name = "NullAudio")
public class NullAudio extends DeferredAudio implements NullResource {
public NullAudio() {
super(null);
}
}
@@ -0,0 +1,92 @@
package mightypork.gamecore.resources.sounds;
import java.util.HashMap;
import java.util.Map;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.AppAdapter;
import mightypork.gamecore.resources.sounds.players.EffectPlayer;
import mightypork.gamecore.resources.sounds.players.LoopPlayer;
import mightypork.utils.logging.Log;
public class SoundBank extends AppAdapter {
private static final DeferredAudio NO_SOUND = new NullAudio();
private static final LoopPlayer NULL_LOOP = new LoopPlayer(NO_SOUND, 0, 0, null);
private static final EffectPlayer NULL_EFFECT = new EffectPlayer(NO_SOUND, 0, 0, null);
private final Map<String, EffectPlayer> effects = new HashMap<String, EffectPlayer>();
private final Map<String, LoopPlayer> loops = new HashMap<String, LoopPlayer>();
public SoundBank(AppAccess app) {
super(app);
if (snd() == null) throw new NullPointerException("SoundSystem cannot be null.");
}
/**
* Register effect resource
*
* @param key sound key
* @param resource resource path
* @param pitch default pitch (1 = unchanged)
* @param gain default gain (0-1)
*/
public void addEffect(String key, String resource, double pitch, double gain)
{
effects.put(key, snd().createEffect(resource, pitch, gain));
}
/**
* Register loop resource (music / effect loop)
*
* @param key sound key
* @param resource resource path
* @param pitch default pitch (1 = unchanged)
* @param gain default gain (0-1)
* @param fadeIn default time for fadeIn
* @param fadeOut default time for fadeOut
*/
public void addLoop(String key, String resource, double pitch, double gain, double fadeIn, double fadeOut)
{
loops.put(key, snd().createLoop(resource, pitch, gain, fadeIn, fadeOut));
}
/**
* Get a loop player for key
*
* @param key sound key
* @return loop player
*/
public LoopPlayer getLoop(String key)
{
final LoopPlayer p = loops.get(key);
if (p == null) {
Log.w("Requesting unknown sound loop \"" + key + "\".");
return NULL_LOOP;
}
return p;
}
/**
* Get a effect player for key
*
* @param key sound key
* @return effect player
*/
public EffectPlayer getEffect(String key)
{
final EffectPlayer p = effects.get(key);
if (p == null) {
Log.w("Requesting unknown sound effect \"" + key + "\".");
return NULL_EFFECT;
}
return p;
}
}
@@ -0,0 +1,213 @@
package mightypork.gamecore.resources.sounds;
import java.nio.FloatBuffer;
import java.util.HashSet;
import java.util.Set;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.control.Subsystem;
import mightypork.gamecore.control.bus.events.ResourceLoadRequest;
import mightypork.gamecore.control.interf.Updateable;
import mightypork.gamecore.resources.sounds.players.EffectPlayer;
import mightypork.gamecore.resources.sounds.players.LoopPlayer;
import mightypork.utils.math.Calc.Buffers;
import mightypork.utils.math.coord.Coord;
import mightypork.utils.objects.Mutable;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.newdawn.slick.openal.SoundStore;
/**
* Sound system class (only one instance should be made per application)
*
* @author MightyPork
*/
@SuppressWarnings("unchecked")
public class SoundSystem extends Subsystem implements Updateable {
private static final Coord INITIAL_LISTENER_POS = new Coord(0, 0, 0);
private static final int MAX_SOURCES = 256;
private static Coord listener = new Coord();
static {
// initialize sound system
SoundStore.get().setMaxSources(MAX_SOURCES);
SoundStore.get().init();
setListener(INITIAL_LISTENER_POS);
}
/**
* Set listener pos
*
* @param pos
*/
public static void setListener(Coord pos)
{
listener.setTo(pos);
FloatBuffer buf3 = Buffers.alloc(3);
FloatBuffer buf6 = Buffers.alloc(6);
buf3.clear();
Buffers.fill(buf3, (float) pos.x, (float) pos.y, (float) pos.z);
AL10.alListener(AL10.AL_POSITION, buf3);
buf3.clear();
Buffers.fill(buf3, 0, 0, 0);
AL10.alListener(AL10.AL_VELOCITY, buf3);
buf6.clear();
Buffers.fill(buf6, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f);
AL10.alListener(AL10.AL_ORIENTATION, buf6);
buf3 = buf6 = null;
}
public static Coord getListener()
{
return listener;
}
// -- instance --
public final Mutable<Double> masterVolume = new Mutable<Double>(1D);
public final Mutable<Double> effectsVolume = new JointVolume(masterVolume);
public final Mutable<Double> loopsVolume = new JointVolume(masterVolume);
private final Set<LoopPlayer> loopPlayers = new HashSet<LoopPlayer>();
private final Set<DeferredAudio> resources = new HashSet<DeferredAudio>();
public SoundSystem(AppAccess app) {
super(app);
}
@Override
public final void deinit()
{
for (final DeferredAudio r : resources) {
r.destroy();
}
SoundStore.get().clear();
AL.destroy();
}
@Override
public void update(double delta)
{
for (final Updateable lp : loopPlayers) {
lp.update(delta);
}
}
/**
* Create effect resource
*
* @param resource resource path
* @param pitch default pitch (1 = unchanged)
* @param gain default gain (0-1)
* @return player
*/
public EffectPlayer createEffect(String resource, double pitch, double gain)
{
return new EffectPlayer(getResource(resource), pitch, gain, effectsVolume);
}
/**
* Register loop resource (music / effect loop)
*
* @param resource resource path
* @param pitch default pitch (1 = unchanged)
* @param gain default gain (0-1)
* @param fadeIn default time for fadeIn
* @param fadeOut default time for fadeOut
* @return player
*/
public LoopPlayer createLoop(String resource, double pitch, double gain, double fadeIn, double fadeOut)
{
final LoopPlayer p = new LoopPlayer(getResource(resource), pitch, gain, loopsVolume);
p.setFadeTimes(fadeIn, fadeOut);
loopPlayers.add(p);
return p;
}
/**
* Create {@link DeferredAudio} for a resource
*
* @param res a resource name
* @return the resource
* @throws IllegalArgumentException if resource is already registered
*/
private DeferredAudio getResource(String res)
{
final DeferredAudio a = new DeferredAudio(res);
bus().send(new ResourceLoadRequest(a));
if (resources.contains(a)) throw new IllegalArgumentException("Sound resource " + res + " is already registered.");
resources.add(a);
return a;
}
/**
* Fade out all loops (ie. for screen transitions)
*/
public void fadeOutAllLoops()
{
for (final LoopPlayer p : loopPlayers) {
p.fadeOut();
}
}
/**
* Pause all loops (leave volume unchanged)
*/
public void pauseAllLoops()
{
for (final LoopPlayer p : loopPlayers) {
p.pause();
}
}
/**
* Set level of master volume
*
* @param d level
*/
public void setMasterVolume(double d)
{
masterVolume.set(d);
}
/**
* Set level of effects volume
*
* @param d level
*/
public void setEffectsVolume(double d)
{
effectsVolume.set(d);
}
/**
* Set level of music volume
*
* @param d level
*/
public void setMusicVolume(double d)
{
loopsVolume.set(d);
}
}
@@ -0,0 +1,81 @@
package mightypork.gamecore.resources.sounds.players;
import mightypork.gamecore.control.interf.Destroyable;
import mightypork.gamecore.resources.sounds.DeferredAudio;
import mightypork.utils.objects.Mutable;
public abstract class BaseAudioPlayer implements Destroyable {
/** the track */
private final DeferredAudio audio;
/** base gain for sfx */
private final double baseGain;
/** base pitch for sfx */
private final double basePitch;
/** dedicated volume control */
private final Mutable<Double> gainMultiplier;
public BaseAudioPlayer(DeferredAudio track, double baseGain, Mutable<Double> gainMultiplier) {
this(track, 1, baseGain, gainMultiplier);
}
public BaseAudioPlayer(DeferredAudio track, double basePitch, double baseGain, Mutable<Double> gainMultiplier) {
this.audio = track;
this.baseGain = baseGain;
this.basePitch = basePitch;
if (gainMultiplier == null) gainMultiplier = new Mutable<Double>(1D);
this.gainMultiplier = gainMultiplier;
}
@Override
public void destroy()
{
audio.destroy();
}
protected DeferredAudio getAudio()
{
return audio;
}
protected double getGain(double multiplier)
{
return baseGain * gainMultiplier.get() * multiplier;
}
protected double getPitch(double multiplier)
{
return basePitch * multiplier;
}
/**
* Get if audio is valid
*
* @return is valid
*/
protected boolean hasAudio()
{
return (audio != null);
}
public void load()
{
if (hasAudio()) audio.load();
}
}
@@ -0,0 +1,37 @@
package mightypork.gamecore.resources.sounds.players;
import mightypork.gamecore.resources.sounds.DeferredAudio;
import mightypork.utils.math.coord.Coord;
import mightypork.utils.objects.Mutable;
public class EffectPlayer extends BaseAudioPlayer {
public EffectPlayer(DeferredAudio track, double basePitch, double baseGain, Mutable<Double> gainMultiplier) {
super(track, (float) basePitch, (float) baseGain, gainMultiplier);
}
public int play(double pitch, double gain)
{
if (!hasAudio()) return -1;
return getAudio().playAsEffect(getPitch(pitch), getGain(gain), false);
}
public int play(double gain)
{
return play(1, gain);
}
public int play(double pitch, double gain, Coord pos)
{
if (!hasAudio()) return -1;
return getAudio().playAsEffect(getPitch(pitch), getGain(gain), false, pos);
}
}
@@ -0,0 +1,133 @@
package mightypork.gamecore.resources.sounds.players;
import mightypork.gamecore.control.interf.Updateable;
import mightypork.gamecore.control.timing.Pauseable;
import mightypork.gamecore.resources.sounds.DeferredAudio;
import mightypork.utils.math.animation.AnimDouble;
import mightypork.utils.objects.Mutable;
import org.lwjgl.openal.AL10;
public class LoopPlayer extends BaseAudioPlayer implements Updateable, Pauseable {
private int sourceID = -1;
/** animator for fade in and fade out */
private final AnimDouble fadeAnim = new AnimDouble(0);
private double lastUpdateGain = 0;
/** flag that track is paused */
private boolean paused = true;
/** Default fadeIn time */
private double inTime = 1;
/** Default fadeOut time */
private double outTime = 1;
public LoopPlayer(DeferredAudio track, double pitch, double baseGain, Mutable<Double> gainMultiplier) {
super(track, (float) pitch, (float) baseGain, gainMultiplier);
paused = true;
}
public void setFadeTimes(double in, double out)
{
inTime = in;
outTime = out;
}
private void initLoop()
{
if (hasAudio() && sourceID == -1) {
sourceID = getAudio().playAsEffect(getPitch(1), getGain(1), true);
getAudio().pauseLoop();
}
}
@Override
public void pause()
{
if (!hasAudio() || paused) return;
initLoop();
getAudio().pauseLoop();
paused = true;
}
@Override
public boolean isPaused()
{
return paused;
}
@Override
public void resume()
{
if (!hasAudio() || !paused) return;
initLoop();
sourceID = getAudio().resumeLoop();
paused = false;
}
@Override
public void update(double delta)
{
if (!hasAudio() || paused) return;
initLoop();
fadeAnim.update(delta);
final double gain = getGain(fadeAnim.now());
if (!paused && gain != lastUpdateGain) {
AL10.alSourcef(sourceID, AL10.AL_GAIN, (float) gain);
lastUpdateGain = gain;
}
if (gain == 0 && !paused) pause(); // pause on zero volume
}
public void fadeIn(double secs)
{
if (!hasAudio()) return;
resume();
fadeAnim.fadeIn(secs);
}
public void fadeOut(double secs)
{
if (!hasAudio()) return;
fadeAnim.fadeOut(secs);
}
public void fadeIn()
{
fadeIn(inTime);
}
public void fadeOut()
{
fadeOut(outTime);
}
}
@@ -0,0 +1,209 @@
package mightypork.gamecore.resources.textures;
import mightypork.gamecore.loading.BaseDeferredResource;
import mightypork.gamecore.loading.MustLoadInMainThread;
import mightypork.gamecore.render.Render;
import mightypork.utils.logging.LoggedName;
import mightypork.utils.math.coord.Rect;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.opengl.Texture;
/**
* Deferred texture
*
* @author MightyPork
*/
@MustLoadInMainThread
@LoggedName(name = "Texture")
public class DeferredTexture extends BaseDeferredResource implements FilteredTexture {
private Texture backingTexture;
private Filter filter_min = Filter.LINEAR;
private Filter filter_mag = Filter.LINEAR;
private Wrap wrap = Wrap.CLAMP;
public DeferredTexture(String resourcePath) {
super(resourcePath);
}
public TxQuad getQuad(Rect rect)
{
return new TxQuad(this, rect);
}
@Override
protected void loadResource(String path)
{
backingTexture = Render.loadTexture(path);
}
@Override
public boolean hasAlpha()
{
if (!ensureLoaded()) return false;
return backingTexture.hasAlpha();
}
public void bindRaw()
{
if (!ensureLoaded()) return;
backingTexture.bind();
}
@Override
public void bind()
{
if (!ensureLoaded()) return;
backingTexture.bind();
GL11.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, wrap.num);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, wrap.num);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter_min.num);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, filter_mag.num);
}
@Override
public int getImageHeight()
{
if (!ensureLoaded()) return 0;
return backingTexture.getImageHeight();
}
@Override
public int getImageWidth()
{
if (!ensureLoaded()) return 0;
return backingTexture.getImageWidth();
}
@Override
public String getTextureRef()
{
if (!ensureLoaded()) return null;
return backingTexture.getTextureRef();
}
@Override
public float getHeight()
{
if (!ensureLoaded()) return 0;
return backingTexture.getHeight();
}
@Override
public float getWidth()
{
if (!ensureLoaded()) return 0;
return backingTexture.getWidth();
}
@Override
public int getTextureHeight()
{
if (!ensureLoaded()) return 0;
return backingTexture.getTextureHeight();
}
@Override
public int getTextureWidth()
{
if (!ensureLoaded()) return 0;
return backingTexture.getTextureWidth();
}
@Override
public void release()
{
if (!isLoaded()) return;
backingTexture.release();
}
@Override
public int getTextureID()
{
if (!ensureLoaded()) return -1;
return backingTexture.getTextureID();
}
@Override
public byte[] getTextureData()
{
if (!ensureLoaded()) return null;
return backingTexture.getTextureData();
}
@Override
public void setTextureFilter(int textureFilter)
{
if (!ensureLoaded()) return;
backingTexture.setTextureFilter(textureFilter);
}
@Override
public void destroy()
{
release();
}
@Override
public void setFilter(Filter filterMin, Filter filterMag)
{
this.filter_min = filterMin;
this.filter_mag = filterMag;
}
@Override
public void setWrap(Wrap wrapping)
{
this.wrap = wrapping;
}
@Override
public void setFilter(Filter filter)
{
setFilter(filter, filter);
}
}
@@ -0,0 +1,56 @@
package mightypork.gamecore.resources.textures;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.opengl.Texture;
public interface FilteredTexture extends Texture {
public static enum Filter
{
LINEAR(GL11.GL_LINEAR), NEAREST(GL11.GL_NEAREST);
public final int num;
private Filter(int gl) {
this.num = gl;
}
}
public static enum Wrap
{
CLAMP(GL11.GL_CLAMP), REPEAT(GL11.GL_REPEAT);
public final int num;
private Wrap(int gl) {
this.num = gl;
}
}
/**
* Set filter for scaling
*
* @param filterMin downscale filter
* @param filterMag upscale filter
*/
void setFilter(Filter filterMin, Filter filterMag);
/**
* Set filter for scaling (both up and down)
*
* @param filter filter
*/
void setFilter(Filter filter);
/**
* @param wrapping wrap mode
*/
void setWrap(Wrap wrapping);
}
@@ -0,0 +1,136 @@
package mightypork.gamecore.resources.textures;
import java.util.HashMap;
import mightypork.gamecore.AppAccess;
import mightypork.gamecore.AppAdapter;
import mightypork.gamecore.control.bus.events.ResourceLoadRequest;
import mightypork.gamecore.resources.textures.FilteredTexture.Filter;
import mightypork.gamecore.resources.textures.FilteredTexture.Wrap;
import mightypork.utils.math.coord.Rect;
import org.newdawn.slick.opengl.Texture;
/**
* Texture loader and quad registry
*
* @author MightyPork
*/
public class TextureBank extends AppAdapter {
public TextureBank(AppAccess app) {
super(app);
}
private final HashMap<String, DeferredTexture> textures = new HashMap<String, DeferredTexture>();
private final HashMap<String, TxQuad> quads = new HashMap<String, TxQuad>();
private DeferredTexture lastTx;
/**
* Load a {@link Texture} from resource, with filters LINEAR and wrap CLAMP
*
* @param key texture key
* @param resourcePath texture resource path
*/
public void loadTexture(String key, String resourcePath)
{
loadTexture(key, resourcePath, Filter.LINEAR, Filter.NEAREST, Wrap.CLAMP);
}
/**
* Load a {@link Texture} from resource
*
* @param key texture key
* @param resourcePath texture resource path
* @param filter_min min filter (when rendered smaller)
* @param filter_mag mag filter (when rendered larger)
* @param wrap texture wrapping
*/
public void loadTexture(String key, String resourcePath, Filter filter_min, Filter filter_mag, Wrap wrap)
{
final DeferredTexture tx = new DeferredTexture(resourcePath);
tx.setFilter(filter_min, filter_mag);
tx.setWrap(wrap);
bus().send(new ResourceLoadRequest(tx));
textures.put(key, tx);
lastTx = tx;
makeQuad(key, Rect.one());
}
/**
* Create a {@link TxQuad} in a texture
*
* @param quadKey quad key
* @param textureKey texture key
* @param quad quad rectangle (absolute pixel coordinates) *
*/
public void makeQuad(String quadKey, String textureKey, Rect quad)
{
final DeferredTexture tx = textures.get(textureKey);
if (tx == null) throw new RuntimeException("Texture with key " + textureKey + " not defined!");
final TxQuad txquad = tx.getQuad(quad);
quads.put(quadKey, txquad);
}
/**
* Create a {@link TxQuad} in the last loaded texture
*
* @param quadKey quad key
* @param quad quad rectangle (absolute pixel coordinates)
*/
public void makeQuad(String quadKey, Rect quad)
{
final DeferredTexture tx = lastTx;
if (tx == null) throw new RuntimeException("There's no texture loaded yet, can't define quads!");
final TxQuad txquad = tx.getQuad(quad);
quads.put(quadKey, txquad);
}
/**
* Get a {@link TxQuad} for key
*
* @param key quad key
* @return the quad
*/
public TxQuad getTxQuad(String key)
{
final TxQuad q = quads.get(key);
if (q == null) throw new RuntimeException("There's no quad called " + key + "!");
return q;
}
/**
* Get a loaded {@link Texture}
*
* @param key texture key
* @return the texture
*/
public Texture getTexture(String key)
{
final Texture t = textures.get(key);
if (t == null) throw new RuntimeException("There's no texture called " + key + "!");
return t;
}
}
@@ -0,0 +1,91 @@
package mightypork.gamecore.resources.textures;
import mightypork.utils.math.coord.Rect;
import org.newdawn.slick.opengl.Texture;
/**
* Texture Quad (describing a part of a texture)
*
* @author MightyPork
*/
public class TxQuad {
/** The texture */
public final Texture tx;
/** Coords in texture (0-1) */
public final Rect uvs;
/**
* TxQuad from origin and size in pixels
*
* @param tx texture
* @param xPx left top X (0-1)
* @param yPx left top Y (0-1)
* @param widthPx area width (0-1)
* @param heightPx area height (0-1)
* @return new TxQuad
*/
public static TxQuad fromSizePx(Texture tx, double xPx, double yPx, double widthPx, double heightPx)
{
final double w = tx.getImageWidth();
final double h = tx.getImageHeight();
return fromSize(tx, xPx / w, yPx / h, widthPx / w, heightPx / h);
}
/**
* TxQuad from origin and size 0-1
*
* @param tx texture
* @param x1 left top X (0-1)
* @param y1 left top Y (0-1)
* @param width area width (0-1)
* @param height area height (0-1)
* @return new TxQuad
*/
public static TxQuad fromSize(Texture tx, double x1, double y1, double width, double height)
{
return new TxQuad(tx, x1, y1, x1 + width, y1 + height);
}
/**
* Make of coords
*
* @param tx texture
* @param x1 left top X (0-1)
* @param y1 left top Y (0-1)
* @param x2 right bottom X (0-1)
* @param y2 right bottom Y (0-1)
*/
public TxQuad(Texture tx, double x1, double y1, double x2, double y2) {
this(tx, new Rect(x1, y1, x2, y2));
}
/**
* @param tx Texture
* @param uvs Rect of texture UVs (0-1)
*/
public TxQuad(Texture tx, Rect uvs) {
this.tx = tx;
this.uvs = uvs.copy();
}
public TxQuad(TxQuad txQuad) {
this.tx = txQuad.tx;
this.uvs = txQuad.uvs.copy();
}
public TxQuad copy()
{
return new TxQuad(this);
}
}