FunkinSprite overhaul

This commit is contained in:
Abnormal 2025-03-04 12:39:11 -06:00
parent d31ef12363
commit b7d132151f
15 changed files with 207 additions and 227 deletions

View file

@ -47,7 +47,8 @@ class FunkTrail extends FlxTrail
{
var targ:Bopper = cast target;
@:privateAccess
frameOffset.set((targ.animOffsets[0] - targ.globalOffsets[0]) * targ.scale.x, (targ.animOffsets[1] - targ.globalOffsets[1]) * targ.scale.y);
frameOffset.set((targ.currentAnimationOffsets[0] - targ.globalOffsets[0]) * targ.scale.x,
(targ.currentAnimationOffsets[1] - targ.globalOffsets[1]) * targ.scale.y);
_recentPositions[0]?.subtract(frameOffset.x, frameOffset.y);
}

View file

@ -0,0 +1,30 @@
package funkin.graphics;
import funkin.graphics.FunkinSprite;
import flixel.animation.FlxAnimationController;
/**
* A version of `FlxAnimationController` that has custom offsets support.
*/
class FunkinAnimationController extends FlxAnimationController
{
/**
* The sprite that this animation controller is attached to.
*/
var _parentSprite:FunkinSprite;
public function new(sprite:FunkinSprite)
{
super(sprite);
_parentSprite = sprite;
}
/**
* We override `FlxAnimationController`'s `play` method to account for animation offsets.
*/
public override function play(animName:String, force = false, reversed = false, frame = 0):Void
{
_parentSprite.applyAnimationOffsets(animName);
super.play(animName, force, reversed, frame);
}
}

View file

@ -10,6 +10,7 @@ import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
import funkin.graphics.FunkinAnimationController;
import flixel.FlxCamera;
/**
@ -31,6 +32,46 @@ class FunkinSprite extends FlxSprite
*/
static var previousCachedTextures:Map<String, FlxGraphic> = [];
/**
* A map of offsets for each animation.
*/
public var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
/**
* The current animation offset being used.
*/
public var currentAnimationOffsets(default, set):Array<Float> = [0, 0];
/**
* Sets the current animation offset.
* Override this in your class if you want to handle animation offsets differently.
*/
function set_currentAnimationOffsets(value:Array<Float>):Array<Float>
{
if (currentAnimationOffsets == null) currentAnimationOffsets = [0, 0];
if (value == null) value = [0, 0];
if ((currentAnimationOffsets[0] == value[0]) && (currentAnimationOffsets[1] == value[1])) return value;
return currentAnimationOffsets = value;
}
/**
* The offset of the sprite overall.
*/
public var globalOffsets(default, set):Array<Float> = [0, 0];
/**
* Sets the global offset.
* Override this in your class if you want to handle global offsets differently.
*/
function set_globalOffsets(value:Array<Float>):Array<Float>
{
if (globalOffsets == null) globalOffsets = [0, 0];
if (globalOffsets == value) return value;
return globalOffsets = value;
}
/**
* @param x Starting X position
* @param y Starting Y position
@ -38,6 +79,16 @@ class FunkinSprite extends FlxSprite
public function new(?x:Float = 0, ?y:Float = 0)
{
super(x, y);
globalOffsets = [x, y];
}
override function initVars():Void
{
super.initVars();
// We replace `FlxSprite`'s default animation controller with our own to handle offsets.
animation.destroy();
animation = new FunkinAnimationController(this);
}
/**
@ -237,17 +288,59 @@ class FunkinSprite extends FlxSprite
}
}
/**
* Ensures the sparrow atlas with the given key is cached.
* @param key The key of the sparrow atlas to cache.
*/
public static function cacheSparrow(key:String):Void
{
cacheTexture(Paths.image(key));
}
/**
* Ensures the packer atlas with the given key is cached.
* @param key The key of the packer atlas to cache.
*/
public static function cachePacker(key:String):Void
{
cacheTexture(Paths.image(key));
}
/**
* Applies the offsets for a specific animation.
* @param animName The animation name.
*/
public function applyAnimationOffsets(animName:String):Void
{
var offsets = animationOffsets.get(animName);
this.currentAnimationOffsets = offsets;
}
/**
* Define the animation offsets for a specific animation.
* @param name The animation name.
* @param xOffset The x offset.
* @param yOffset The y offset.
*/
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
{
animationOffsets.set(name, [xOffset, yOffset]);
}
/**
* Set the sprite scale to the appropriate value.
* @param scale
*/
public function setScale(scale:Null<Float>):Void
{
if (scale == null) scale = 1.0;
this.scale.x = scale;
this.scale.y = scale;
this.updateHitbox();
}
/**
* Prepares the sprite cache for purging.
* Call this, then `cacheTexture` to keep the textures we still need, then `purgeCache` to remove the textures that we won't be using anymore.
*/
public static function preparePurgeCache():Void
@ -256,6 +349,9 @@ class FunkinSprite extends FlxSprite
currentCachedTextures = [];
}
/**
* Purges the old sprite cache.
*/
public static function purgeCache():Void
{
// Everything that is in previousCachedTextures but not in currentCachedTextures should be destroyed.
@ -269,6 +365,11 @@ class FunkinSprite extends FlxSprite
}
}
/**
* Whether or not the given graphic is cached.
* @param graphic The graphic to check.
* @return Bool
*/
static function isGraphicCached(graphic:FlxGraphic):Bool
{
if (graphic == null) return false;
@ -283,6 +384,7 @@ class FunkinSprite extends FlxSprite
}
/**
* Whether or not the given animation is dynamic (has multiple frames).
* @param id The animation ID to check.
* @return Whether the animation is dynamic (has multiple frames). `false` for static, one-frame animations.
*/
@ -294,6 +396,37 @@ class FunkinSprite extends FlxSprite
return animData.numFrames > 1;
}
/**
* Checks whether or not the given animation exists for this sprite.
* @param id The name of the animation to check for.
* @return Whether this sprite posesses the given animation.
* Only true if the animation was successfully loaded from the XML.
*/
public function hasAnimation(id:String):Bool
{
if (this.animation == null) return false;
return this.animation.getByName(id) != null;
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the sprite is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
return this.animation?.curAnim?.name ?? "";
}
/**
* Whether the current animation has finished playing.
*/
public function isAnimationFinished():Bool
{
return this.animation?.finished ?? false;
}
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
* at the expense of not being able to paint onto the resulting sprite.
@ -367,6 +500,10 @@ class FunkinSprite extends FlxSprite
if (camera == null) camera = FlxG.camera;
result.set(x, y);
result.x -= currentAnimationOffsets[0];
result.y -= currentAnimationOffsets[1];
if (pixelPerfectPosition)
{
_rect.width = _rect.width / this.scale.x;

View file

@ -250,14 +250,10 @@ class BaseCharacter extends Bopper
* Set the character's sprite scale to the appropriate value.
* @param scale The desired scale.
*/
public function setScale(scale:Null<Float>):Void
public override function setScale(scale:Null<Float>):Void
{
if (scale == null) scale = 1.0;
super.setScale(scale);
var feetPos:FlxPoint = feetPosition;
this.scale.x = scale;
this.scale.y = scale;
this.updateHitbox();
// Reposition with newly scaled sprite.
this.x = feetPos.x - characterOrigin.x + globalOffsets[0];
this.y = feetPos.y - characterOrigin.y + globalOffsets[1];

View file

@ -114,7 +114,7 @@ class MultiSparrowCharacter extends BaseCharacter
if (anim.offsets == null)
{
setAnimationOffsets(anim.name, 0, 0);
setAnimationOffsets(anim.name, 0.0, 0.0);
}
else
{

View file

@ -71,7 +71,7 @@ class SparrowCharacter extends BaseCharacter
{
if (anim.offsets == null)
{
setAnimationOffsets(anim.name, 0, 0);
setAnimationOffsets(anim.name, 0.0, 0.0);
}
else
{

View file

@ -450,35 +450,6 @@ class HealthIcon extends FunkinSprite
this.antialiasing = !isPixel;
}
/**
* @return Name of the current animation being played by this health icon.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
/**
* @param id The name of the animation to check for.
* @return Whether this sprite posesses the given animation.
* Only true if the animation was successfully loaded from the XML.
*/
public function hasAnimation(id:String):Bool
{
if (this.animation == null) return false;
return this.animation.getByName(id) != null;
}
/**
* @return Whether the current animation is in the finished state.
*/
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
/**
* Plays the animation with the given name.
* @param name The name of the animation to play.

View file

@ -126,7 +126,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.boxSprite = null;
}
this.boxSprite = new FunkinSprite(0, 0);
this.boxSprite = new FlxSprite(0, 0);
trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');

View file

@ -1,6 +1,6 @@
package funkin.play.cutscene.dialogue;
import flixel.FlxSprite;
import funkin.graphics.FunkinSprite;
import funkin.data.IRegistryEntry;
import funkin.modding.events.ScriptEvent;
import flixel.graphics.frames.FlxFramesCollection;
@ -14,7 +14,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
*
* Most conversations have two speakers, with one being flipped.
*/
class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
class Speaker extends FunkinSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
{
/**
* The internal ID for this speaker.
@ -36,36 +36,21 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
return _data.name;
}
/**
* Offset the speaker's sprite by this much when playing each animation.
*/
var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
/**
* The current animation offset being used.
*/
var animOffsets(default, set):Array<Float> = [0, 0];
function set_animOffsets(value:Array<Float>):Array<Float>
override function set_currentAnimationOffsets(value:Array<Float>):Array<Float>
{
if (animOffsets == null) animOffsets = [0, 0];
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
if (currentAnimationOffsets == null) currentAnimationOffsets = [0, 0];
if ((currentAnimationOffsets[0] == value[0]) && (currentAnimationOffsets[1] == value[1])) return value;
var xDiff:Float = value[0] - animOffsets[0];
var yDiff:Float = value[1] - animOffsets[1];
var xDiff:Float = value[0] - currentAnimationOffsets[0];
var yDiff:Float = value[1] - currentAnimationOffsets[1];
this.x += xDiff;
this.y += yDiff;
return animOffsets = value;
return currentAnimationOffsets = value;
}
/**
* The offset of the speaker overall.
*/
public var globalOffsets(default, set):Array<Float> = [0, 0];
function set_globalOffsets(value:Array<Float>):Array<Float>
override function set_globalOffsets(value:Array<Float>):Array<Float>
{
if (globalOffsets == null) globalOffsets = [0, 0];
if (globalOffsets == value) return value;
@ -154,18 +139,6 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
this.setScale(_data.scale);
}
/**
* Set the sprite scale to the appropriate value.
* @param scale
*/
public function setScale(scale:Null<Float>):Void
{
if (scale == null) scale = 1.0;
this.scale.x = scale;
this.scale.y = scale;
this.updateHitbox();
}
function loadAnimations():Void
{
trace('[SPEAKER] Loading ${_data.animations.length} animations for ${id}');
@ -176,7 +149,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
{
if (anim.offsets == null)
{
setAnimationOffsets(anim.name, 0, 0);
setAnimationOffsets(anim.name, 0.0, 0.0);
}
else
{
@ -198,14 +171,6 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
if (correctName == null) return;
this.animation.play(correctName, restart, false, 0);
applyAnimationOffsets(correctName);
}
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
/**
@ -242,37 +207,6 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
}
}
public function hasAnimation(id:String):Bool
{
if (this.animation == null) return false;
return this.animation.getByName(id) != null;
}
/**
* Define the animation offsets for a specific animation.
*/
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
{
animationOffsets.set(name, [xOffset, yOffset]);
}
/**
* Retrieve an apply the animation offsets for a specific animation.
*/
function applyAnimationOffsets(name:String):Void
{
var offsets:Array<Float> = animationOffsets.get(name);
if (offsets != null && !(offsets[0] == 0 && offsets[1] == 0))
{
this.animOffsets = offsets;
}
else
{
this.animOffsets = [0, 0];
}
}
public function onDialogueStart(event:DialogueScriptEvent):Void {}
public function onDialogueCompleteLine(event:DialogueScriptEvent):Void {}

View file

@ -167,22 +167,6 @@ class StrumlineNote extends FunkinSprite
}
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the sprite is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
static final DEFAULT_OFFSET:Int = 13;
/**

View file

@ -32,11 +32,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
*/
public var shouldAlternate:Null<Bool> = null;
/**
* Offset the character's sprite by this much when playing each animation.
*/
public var animationOffsets:Map<String, Array<Float>> = new Map<String, Array<Float>>();
/**
* Add a suffix to the `idle` animation (or `danceLeft` and `danceRight` animations)
* that this bopper will play.
@ -68,32 +63,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
return value;
}
/**
* The offset of the character relative to the position specified by the stage.
*/
public var globalOffsets(default, set):Array<Float> = [0, 0];
function set_globalOffsets(value:Array<Float>):Array<Float>
{
if (globalOffsets == null) globalOffsets = [0, 0];
if (globalOffsets == value) return value;
return globalOffsets = value;
}
@:allow(funkin.ui.debug.anim.DebugBoundingState)
var animOffsets(default, set):Array<Float> = [0, 0];
public var originalPosition:FlxPoint = new FlxPoint(0, 0);
function set_animOffsets(value:Array<Float>):Array<Float>
{
if (animOffsets == null) animOffsets = [0, 0];
if ((animOffsets[0] == value[0]) && (animOffsets[1] == value[1])) return value;
return animOffsets = value;
}
/**
* Whether to play `danceRight` next iteration.
* Only used when `shouldAlternate` is true.
@ -203,13 +174,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
}
}
public function hasAnimation(id:String):Bool
{
if (this.animation == null) return false;
return this.animation.getByName(id) != null;
}
/**
* Ensure that a given animation exists before playing it.
* Will gracefully check for name, then name with stripped suffixes, then fail to play.
@ -293,8 +257,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
{
canPlayOtherAnims = false;
}
applyAnimationOffsets(correctName);
}
var forceAnimationTimer:FlxTimer = new FlxTimer();
@ -313,7 +275,6 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
if (correctName == null) return;
this.animation.play(correctName, false, false);
applyAnimationOffsets(correctName);
canPlayOtherAnims = false;
forceAnimationTimer.start(duration, (timer) -> {
@ -321,40 +282,18 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
}, 1);
}
function applyAnimationOffsets(name:String):Void
{
var offsets = animationOffsets.get(name);
this.animOffsets = offsets;
}
public function isAnimationFinished():Bool
{
return this.animation?.finished ?? false;
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
{
animationOffsets.set(name, [xOffset, yOffset]);
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the character is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
// override getScreenPosition (used by FlxSprite's draw method) to account for animation offsets.
override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
{
var output:FlxPoint = super.getScreenPosition(result, camera);
output.x -= (animOffsets[0] - globalOffsets[0]) * this.scale.x;
output.y -= (animOffsets[1] - globalOffsets[1]) * this.scale.y;
return output;
if (result == null) result = FlxPoint.get();
if (camera == null) camera = getDefaultCamera();
result.set(x, y);
result.x -= (currentAnimationOffsets[0] - globalOffsets[0]) * this.scale.x;
result.y -= (currentAnimationOffsets[1] - globalOffsets[1]) * this.scale.y;
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
}
public function onPause(event:PauseScriptEvent) {}

View file

@ -214,18 +214,18 @@ class DebugBoundingState extends FlxState
if (FlxG.mouse.justPressed && !haxeUIFocused)
{
movingCharacter = true;
mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]);
mouseOffset.set(FlxG.mouse.x - -swagChar.currentAnimationOffsets[0], FlxG.mouse.y - -swagChar.currentAnimationOffsets[1]);
}
if (!movingCharacter) return;
if (FlxG.mouse.pressed)
{
swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1];
swagChar.currentAnimationOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1];
swagChar.animationOffsets.set(offsetAnimationDropdown.value.id, swagChar.animOffsets);
swagChar.animationOffsets.set(offsetAnimationDropdown.value.id, swagChar.currentAnimationOffsets);
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.text = 'Offset: ' + swagChar.currentAnimationOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
}
@ -532,7 +532,7 @@ class DebugBoundingState extends FlxState
playCharacterAnimation(event.data.id, true);
}
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.text = 'Offset: ' + swagChar.currentAnimationOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
dropDownSetup = true;
}
@ -555,7 +555,7 @@ class DebugBoundingState extends FlxState
swagChar.playAnimation(animName, true); // trace();
trace(swagChar.animationOffsets.get(animName));
txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets;
txtOffsetShit.text = 'Offset: ' + swagChar.currentAnimationOffsets;
txtOffsetShit.y = FlxG.height - 20 - txtOffsetShit.height;
}

View file

@ -180,10 +180,10 @@ class CharacterPlayer extends Box
character.y = this.cachedScreenY;
// Apply animation offsets, so the character is positioned correctly based on the animation.
@:privateAccess var animOffsets:Array<Float> = character.animOffsets;
@:privateAccess var currentAnimationOffsets:Array<Float> = character.currentAnimationOffsets;
character.x -= animOffsets[0] * targetScale * (flip ? -1 : 1);
character.y -= animOffsets[1] * targetScale;
character.x -= currentAnimationOffsets[0] * targetScale * (flip ? -1 : 1);
character.y -= currentAnimationOffsets[1] * targetScale;
}
/**

View file

@ -187,7 +187,7 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
*/
function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(25, (120 * (items.length - 1 + 1)) + 35, defaultValue);
items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
var value = !checkbox.currentValue;

View file

@ -1,8 +1,8 @@
package funkin.ui.options.items;
import flixel.FlxSprite.FlxSprite;
import funkin.graphics.FunkinSprite;
class CheckboxPreferenceItem extends FlxSprite
class CheckboxPreferenceItem extends FunkinSprite
{
public var currentValue(default, set):Bool;
@ -10,9 +10,10 @@ class CheckboxPreferenceItem extends FlxSprite
{
super(x, y);
frames = Paths.getSparrowAtlas('checkboxThingie');
loadSparrow('checkboxThingie');
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
setAnimationOffsets('checked', 17, 70);
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
@ -20,19 +21,6 @@ class CheckboxPreferenceItem extends FlxSprite
this.currentValue = defaultValue;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
switch (animation.curAnim.name)
{
case 'static':
offset.set();
case 'checked':
offset.set(17, 70);
}
}
function set_currentValue(value:Bool):Bool
{
if (value)