Animated movements !

Enhancements in debug map graphics, and more monster to better test
animations.
Player slides from tile to tile in MIN_INPUT_DEPLAY / 2 ms.
Mobs slide at a speed proportional to their Move cost / Max AP ratio.
Animation refresh rate is 25ms.
This commit is contained in:
Zukero
2015-04-10 11:40:17 +02:00
parent e4575f59cb
commit c5cddf96ba
12 changed files with 231 additions and 26 deletions

View File

@@ -62,7 +62,7 @@
"monsterClass": "insect",
"maxHP": 10,
"maxAP": 10,
"moveCost": 10,
"moveCost": 5,
"attackCost": 10,
"attackChance": 50,
"droplistID": "debuglist1",

View File

@@ -122,12 +122,12 @@
</tileset>
<layer name="Ground" width="10" height="10">
<data encoding="base64" compression="zlib">
eJw7ysbAcHSQ4ydUNOsEEqalultkmgcAw35PpQ==
eJw7ysbAcHQQ4wYOBoYnVDTvBBKmpbpbZJoHAISsT2I=
</data>
</layer>
<layer name="Objects" width="10" height="10">
<data encoding="base64" compression="zlib">
eJxjYMAOktlxSNAYSFDRXlsm6plFLwAA6HwAyQ==
eJyby88AB7FI7JvMDBSDbDLMyOQnrIZYYMtEPbNIBfPI9AcAQ5QDxw==
</data>
</layer>
<layer name="Above" width="10" height="10">
@@ -140,7 +140,7 @@
eJxjYBi+wFVgoF1AOgAAPUQAVg==
</data>
</layer>
<objectgroup name="Object Layer 1">
<objectgroup name="Object Layer 1" width="0" height="0">
<object name="debugsign" type="sign" x="192" y="96" width="32" height="32"/>
<object name="start" type="rest" x="96" y="64" width="32" height="32"/>
<object name="Unopenable key area" type="key" x="32" y="128" width="32" height="32">
@@ -181,10 +181,10 @@
</properties>
</object>
</objectgroup>
<objectgroup name="Spawn">
<objectgroup name="Spawn" width="0" height="0">
<object name="insect" type="spawn" x="96" y="128" width="192" height="160">
<properties>
<property name="quantity" value="2"/>
<property name="quantity" value="15"/>
</properties>
</object>
<object name="troll" type="spawn" x="256" y="256" width="64" height="64">

View File

@@ -32,7 +32,7 @@ public final class Constants {
public static final int TICKS_PER_FULLROUND = FULLROUND_DURATION / TICK_DELAY;
public static final int SPLATTER_DURATION_MS = 20000;
public static final ConstRange monsterWaitTurns = new ConstRange(30,4);
public static final ConstRange monsterWaitTurns = new ConstRange(5,1);
public static final long MAP_UNVISITED_RESPAWN_DURATION_MS = 3 * 60 * 1000; // 3 min in milliseconds
public static final String PREFERENCE_MODEL_LASTRUNVERSION = "lastversion";

View File

@@ -74,7 +74,7 @@ public final class MonsterMovementController implements EvaluateWalkable {
private void moveMonster(final Monster m, final MonsterSpawnArea area) {
PredefinedMap map = world.model.currentMap;
LayeredTileMap tileMap = world.model.currentTileMap;
m.nextActionTime += getMillisecondsPerMove(m);
m.nextActionTime = System.currentTimeMillis() + getMillisecondsPerMove(m);
if (m.movementDestination == null) {
// Monster has waited and should start to move again.
m.movementDestination = new Coord(m.position);
@@ -128,7 +128,7 @@ public final class MonsterMovementController implements EvaluateWalkable {
private static void cancelCurrentMonsterMovement(final Monster m) {
m.movementDestination = null;
m.nextActionTime += getMillisecondsPerMove(m) * Constants.rollValue(Constants.monsterWaitTurns);
m.nextActionTime = System.currentTimeMillis() + (getMillisecondsPerMove(m) * Constants.rollValue(Constants.monsterWaitTurns));
}
private static int getMillisecondsPerMove(Monster m) {
@@ -151,9 +151,17 @@ public final class MonsterMovementController implements EvaluateWalkable {
return monsterCanMoveTo(world.model.currentMap, world.model.currentTileMap, r);
}
public void moveMonsterToNextPosition(Monster m, PredefinedMap map) {
CoordRect previousPosition = new CoordRect(new Coord(m.position), m.rectPosition.size);
public void moveMonsterToNextPosition(final Monster m, final PredefinedMap map) {
final CoordRect previousPosition = new CoordRect(new Coord(m.position), m.rectPosition.size);
m.lastPosition.set(previousPosition.topLeft);
m.position.set(m.nextPosition.topLeft);
monsterMovementListeners.onMonsterMoved(map, m, previousPosition);
controllers.effectController.startActorMoveEffect(m, previousPosition.topLeft, m.position, getMillisecondsPerMove(m) / 4, new VisualEffectController.VisualEffectCompletedCallback() {
@Override
public void onVisualEffectCompleted(int callbackValue) {
monsterMovementListeners.onMonsterMoved(map, m, previousPosition);
}
}, 0);
}
}

View File

@@ -228,15 +228,24 @@ public final class MovementController implements TimedMessageTask.Callback {
player.lastPosition.set(player.position);
player.position.set(newPosition);
controllers.combatController.setCombatSelection(null, null);
playerMovementListeners.onPlayerMoved(newPosition, player.lastPosition);
controllers.effectController.startActorMoveEffect(player, player.lastPosition, newPosition, (int) (Constants.MINIMUM_INPUT_INTERVAL / 2), new VisualEffectController.VisualEffectCompletedCallback() {
@Override
public void onVisualEffectCompleted(int callbackValue) {
playerMovementListeners.onPlayerMoved(newPosition, player.lastPosition);
controllers.mapController.handleMapEventsAfterMovement(currentMap, newPosition, player.lastPosition);
controllers.mapController.handleMapEventsAfterMovement(currentMap, newPosition, player.lastPosition);
if (!world.model.uiSelections.isInCombat) {
//currentMap can be outdated due to mapchange events processed above.
Loot loot = world.model.currentMap.getBagAt(newPosition);
if (loot != null) controllers.itemController.playerSteppedOnLootBag(loot);
}
if (!world.model.uiSelections.isInCombat) {
//currentMap can be outdated due to mapchange events processed above.
Loot loot = world.model.currentMap.getBagAt(newPosition);
if (loot != null) controllers.itemController.playerSteppedOnLootBag(loot);
}
}
}, 0);
}
public void respawnPlayer(Resources res) {

View File

@@ -4,9 +4,11 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.os.Handler;
import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.listeners.VisualEffectFrameListeners;
import com.gpl.rpg.AndorsTrail.model.actor.Actor;
import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.actor.MonsterType;
import com.gpl.rpg.AndorsTrail.model.map.PredefinedMap;
@@ -54,7 +56,70 @@ public final class VisualEffectController {
enqueuedEffectID = null;
enqueuedEffectValue = 0;
}
public void startActorMoveEffect(Actor actor, Coord origin, Coord destination, int duration, VisualEffectCompletedCallback callback, int callbackValue) {
++effectCount;
(new SpriteMoveAnimation(origin, destination, duration, actor, callback, callbackValue))
.start();
}
public final class SpriteMoveAnimation extends Handler implements Runnable {
private static final int millisecondsPerFrame=25;
private final VisualEffectCompletedCallback callback;
private final int callbackValue;
public final int duration;
public final Actor actor;
public final Coord origin;
public final Coord destination;
public int timeElapsed;
@Override
public void run() {
update();
if (System.currentTimeMillis() - actor.vfxStartTime >= duration) {
onCompleted();
} else {
postDelayed(this, millisecondsPerFrame);
}
}
public SpriteMoveAnimation(Coord origin, Coord destination, int duration, Actor actor, VisualEffectCompletedCallback callback, int callbackValue) {
this.callback = callback;
this.callbackValue = callbackValue;
this.duration = duration;
this.actor = actor;
this.origin = origin;
this.destination = destination;
this.timeElapsed = 0;
}
private void update() {
visualEffectFrameListeners.onNewSpriteMoveFrame(this);
}
private void onCompleted() {
--effectCount;
actor.hasVFXRunning = false;
if (callback != null) callback.onVisualEffectCompleted(callbackValue);
visualEffectFrameListeners.onSpriteMoveCompleted(this);
}
public void start() {
actor.hasVFXRunning = true;
actor.vfxDuration = duration;
actor.vfxStartTime = System.currentTimeMillis();
postDelayed(this, 0);
}
}
public final class VisualEffectAnimation extends Handler implements Runnable {
@Override

View File

@@ -1,8 +1,11 @@
package com.gpl.rpg.AndorsTrail.controller.listeners;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.SpriteMoveAnimation;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.VisualEffectAnimation;
public interface VisualEffectFrameListener {
void onNewAnimationFrame(VisualEffectAnimation animation, int tileID, int textYOffset);
void onAnimationCompleted(VisualEffectAnimation animation);
void onNewSpriteMoveFrame(SpriteMoveAnimation animation);
void onSpriteMoveCompleted(SpriteMoveAnimation animation);
}

View File

@@ -1,5 +1,6 @@
package com.gpl.rpg.AndorsTrail.controller.listeners;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.SpriteMoveAnimation;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.VisualEffectAnimation;
import com.gpl.rpg.AndorsTrail.util.ListOfListeners;
@@ -12,6 +13,14 @@ public final class VisualEffectFrameListeners extends ListOfListeners<VisualEffe
private final Function1<VisualEffectFrameListener, VisualEffectAnimation> onAnimationCompleted = new Function1<VisualEffectFrameListener, VisualEffectAnimation>() {
@Override public void call(VisualEffectFrameListener listener, VisualEffectAnimation animation) { listener.onAnimationCompleted(animation); }
};
private final Function1<VisualEffectFrameListener, SpriteMoveAnimation> onNewSpriteMoveFrame = new Function1<VisualEffectFrameListener, SpriteMoveAnimation>() {
@Override public void call(VisualEffectFrameListener listener, SpriteMoveAnimation animation) { listener.onNewSpriteMoveFrame(animation); }
};
private final Function1<VisualEffectFrameListener, SpriteMoveAnimation> onSpriteMoveCompleted = new Function1<VisualEffectFrameListener, SpriteMoveAnimation>() {
@Override public void call(VisualEffectFrameListener listener, SpriteMoveAnimation animation) { listener.onSpriteMoveCompleted(animation); }
};
@Override
public void onNewAnimationFrame(VisualEffectAnimation animation, int tileID, int textYOffset) {
@@ -22,4 +31,14 @@ public final class VisualEffectFrameListeners extends ListOfListeners<VisualEffe
public void onAnimationCompleted(VisualEffectAnimation animation) {
callAllListeners(this.onAnimationCompleted, animation);
}
@Override
public void onNewSpriteMoveFrame(SpriteMoveAnimation animation) {
callAllListeners(this.onNewSpriteMoveFrame, animation);
}
@Override
public void onSpriteMoveCompleted(SpriteMoveAnimation animation) {
callAllListeners(this.onSpriteMoveCompleted, animation);
}
}

View File

@@ -34,6 +34,10 @@ public class Actor {
public int blockChance;
public int damageResistance;
public ItemTraits_OnUse[] onHitEffects;
public boolean hasVFXRunning = false;
public long vfxStartTime = 0;
public int vfxDuration = 0;
public final Coord lastPosition = new Coord();
public Actor(
Size tileSize

View File

@@ -29,7 +29,6 @@ import com.gpl.rpg.AndorsTrail.util.Size;
public final class Player extends Actor {
public static final int DEFAULT_PLAYER_ATTACKCOST = 4;
public final Coord lastPosition;
public final Coord nextPosition;
// TODO: Should be privates
@@ -87,7 +86,6 @@ public final class Player extends Actor {
, true // isPlayer
, false // isImmuneToCriticalHits
);
this.lastPosition = new Coord();
this.nextPosition = new Coord();
this.levelExperience = new Range();
this.inventory = new Inventory();

View File

@@ -72,4 +72,24 @@ public final class CoordRect {
public String toString() {
return '{' + topLeft.toString() + ", " + size.toString() + '}';
}
public static CoordRect getBoundingRect(Coord c1, Coord c2) {
int x, y, w, h;
if (c2.x < c1.x) {
x = c2.x;
w = 1 + c1.x - c2.x;
} else {
x = c1.x;
w = 1 + c2.x - c1.x;
}
if (c2.y < c1.y) {
y = c2.y;
h = 1 + c1.y - c2.y;
} else {
y = c1.y;
h = 1 + c2.y - c1.y;
}
return new CoordRect(new Coord(x, y), new Size(w, h));
}
}

View File

@@ -10,12 +10,14 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.AndorsTrailPreferences;
import com.gpl.rpg.AndorsTrail.context.ControllerContext;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.controller.InputController;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.BloodSplatter;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.SpriteMoveAnimation;
import com.gpl.rpg.AndorsTrail.controller.VisualEffectController.VisualEffectAnimation;
import com.gpl.rpg.AndorsTrail.controller.listeners.*;
import com.gpl.rpg.AndorsTrail.model.ModelContainer;
@@ -29,6 +31,7 @@ import com.gpl.rpg.AndorsTrail.resource.tiles.TileCollection;
import com.gpl.rpg.AndorsTrail.resource.tiles.TileManager;
import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.CoordRect;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.Size;
public final class MainView extends SurfaceView
@@ -49,7 +52,8 @@ public final class MainView extends SurfaceView
private final Coord screenOffset = new Coord(); // pixel offset where the image begins
private final Coord mapTopLeft = new Coord(); // Map coords of visible map
private CoordRect mapViewArea; // Area in mapcoordinates containing the visible map. topleft == this.topleft
private Rect redrawClip = new Rect(); //Area in screen coordinates containing the visible map.
private final ModelContainer model;
private final WorldContext world;
private final ControllerContext controllers;
@@ -199,6 +203,7 @@ public final class MainView extends SurfaceView
}
}
synchronized (holder) { synchronized (tiles) {
c.clipRect(redrawClip);
c.translate(screenOffset.x, screenOffset.y);
c.scale(scale, scale);
doDrawRect(c, area);
@@ -213,6 +218,34 @@ public final class MainView extends SurfaceView
if (c != null) holder.unlockCanvasAndPost(c);
}
}
private void redrawMoveArea_(CoordRect area, final SpriteMoveAnimation effect) {
if (!hasSurface) return;
if (currentMap.isOutside(area)) return;
if (!mapViewArea.intersects(area)) return;
calculateRedrawRect(area);
Canvas c = null;
try {
c = holder.lockCanvas(redrawRect);
// lockCanvas sometimes changes redrawRect, when the double-buffer has not been
// sufficiently filled beforehand. In those cases, we need to redraw the whole scene.
if (area != mapViewArea) {
if (isRedrawRectWholeScreen(redrawRect)) {
area = mapViewArea;
}
}
synchronized (holder) { synchronized (tiles) {
c.clipRect(redrawClip);
c.translate(screenOffset.x, screenOffset.y);
c.scale(scale, scale);
doDrawRect(c, area);
} }
} finally {
if (c != null) holder.unlockCanvasAndPost(c);
}
}
private boolean isRedrawRectWholeScreen(Rect redrawRect) {
if (redrawRect.width() < mapViewArea.size.width * scaledTileSize) return false;
@@ -256,13 +289,25 @@ public final class MainView extends SurfaceView
}
private void doDrawRect(Canvas canvas, CoordRect area) {
doDrawRect_Below(canvas, area);
doDrawRect_Objects(canvas, area);
doDrawRect_Above(canvas, area);
}
private void doDrawRect_Below(Canvas canvas, CoordRect area) {
drawMapLayer(canvas, area, currentTileMap.currentLayout.layerGround);
tryDrawMapLayer(canvas, area, currentTileMap.currentLayout.layerObjects);
for (BloodSplatter splatter : currentMap.splatters) {
drawFromMapPosition(canvas, area, splatter.position, splatter.iconID);
}
}
private void doDrawRect_Objects(Canvas canvas, CoordRect area) {
for (BloodSplatter splatter : currentMap.splatters) {
drawFromMapPosition(canvas, area, splatter.position, splatter.iconID);
}
for (Loot l : currentMap.groundBags) {
if (l.isVisible) {
@@ -270,13 +315,31 @@ public final class MainView extends SurfaceView
}
}
drawFromMapPosition(canvas, area, playerPosition, model.player.iconID);
if (!model.player.hasVFXRunning) {
drawFromMapPosition(canvas, area, playerPosition, model.player.iconID);
} else if (area.contains(playerPosition)) {
int vfxElapsedTime = (int) (System.currentTimeMillis() - model.player.vfxStartTime);
if (vfxElapsedTime > model.player.vfxDuration) vfxElapsedTime = model.player.vfxDuration;
int x = ((model.player.position.x - mapViewArea.topLeft.x) * tileSize * vfxElapsedTime + ((model.player.lastPosition.x - mapViewArea.topLeft.x) * tileSize * (model.player.vfxDuration - vfxElapsedTime))) / model.player.vfxDuration;
int y = ((model.player.position.y - mapViewArea.topLeft.y) * tileSize * vfxElapsedTime + ((model.player.lastPosition.y - mapViewArea.topLeft.y) * tileSize * (model.player.vfxDuration - vfxElapsedTime))) / model.player.vfxDuration;
tiles.drawTile(canvas, model.player.iconID, x, y, mPaint);
}
for (MonsterSpawnArea a : currentMap.spawnAreas) {
for (Monster m : a.monsters) {
drawFromMapPosition(canvas, area, m.rectPosition, m.iconID);
if (!m.hasVFXRunning) {
drawFromMapPosition(canvas, area, m.rectPosition, m.iconID);
} else if (area.intersects(m.rectPosition) || area.contains(m.lastPosition)) {
int vfxElapsedTime = (int) (System.currentTimeMillis() - m.vfxStartTime);
if (vfxElapsedTime > m.vfxDuration) vfxElapsedTime = m.vfxDuration;
int x = ((m.position.x - mapViewArea.topLeft.x) * tileSize * vfxElapsedTime + ((m.lastPosition.x - mapViewArea.topLeft.x) * tileSize * (m.vfxDuration - vfxElapsedTime))) / m.vfxDuration;
int y = ((m.position.y - mapViewArea.topLeft.y) * tileSize * vfxElapsedTime + ((m.lastPosition.y - mapViewArea.topLeft.y) * tileSize * (m.vfxDuration - vfxElapsedTime))) / m.vfxDuration;
tiles.drawTile(canvas, m.iconID, x, y, mPaint);
}
}
}
}
private void doDrawRect_Above(Canvas canvas, CoordRect area) {
tryDrawMapLayer(canvas, area, currentTileMap.currentLayout.layerAbove);
if (model.uiSelections.selectedPosition != null) {
@@ -342,6 +405,7 @@ public final class MainView extends SurfaceView
,Math.min(screenSizeTileCount.height, currentMap.size.height)
);
mapViewArea = new CoordRect(mapTopLeft, visibleNumberOfTiles);
updateClip();
screenOffset.set(
(surfaceSize.width - scaledTileSize * visibleNumberOfTiles.width) / 2
@@ -370,8 +434,13 @@ public final class MainView extends SurfaceView
mapTopLeft.y = Math.max(0, playerPosition.y - mapViewArea.size.height/2);
mapTopLeft.y = Math.min(mapTopLeft.y, currentMap.size.height - mapViewArea.size.height);
}
updateClip();
}
}
private void updateClip() {
worldCoordsToScreenCords(mapViewArea, redrawClip);
}
@Override
public void onPlayerMoved(Coord newPosition, Coord previousPosition) {
@@ -492,6 +561,16 @@ public final class MainView extends SurfaceView
public void onAnimationCompleted(VisualEffectAnimation animation) {
redrawArea(animation.area, RedrawAreaDebugReason.EffectCompleted);
}
@Override
public void onNewSpriteMoveFrame(SpriteMoveAnimation animation) {
redrawMoveArea_(CoordRect.getBoundingRect(animation.origin, animation.destination), animation);
}
@Override
public void onSpriteMoveCompleted(SpriteMoveAnimation animation) {
redrawArea(CoordRect.getBoundingRect(animation.origin, animation.destination), RedrawAreaDebugReason.EffectCompleted);
}
@Override
public void onNewTick() {