forked from zacharycarter/zengine
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Starting on the sprite implementation
At this moment we are able to read and parse in a very minimal .zsprite file. So far we can do things like define frames, sequences, add looping modifiers, etc. Righut now there is a `Sprite` object, and a `ZSprite` object (which is a child of `Sprite). They are mainly ment to be dumb data containters that only are for managing the state of a sprite object (e.g. timing, which frame to show, etc). In the futur we plane to have sprites that are defined by the `Spline` format, so that's why I made the common `Sprite` base object. In case a game dev also wants to make their own custom sprite they can two sub-object it too. The rendering logic (and OpenGL state) is going to be put inside the `SpriteBatch` object. Right now it's only going to support ZSprites, but in the future will support SplineSprites. Since at the moment there are some other things that need to be decided on (such as sprite coordinate systems) and some other code implemented(e.g. a timing system, instead of using `sdl.getTicks()`), they are only rendering their spritesheets at the moment, as proof that we can load up a ZSprite and show something. The example `03` for sprites isn't also done yet either, so I decided not to include the code. I'm only reserving the name. Don't consider issue zacharycarter#13 done yet. This is only the first step.
- Loading branch information
1 parent
7f6ed99
commit 3485f32
Showing
9 changed files
with
425 additions
and
3 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
# Blau Gest, a blue spinning ghost | ||
v1.0 | ||
blau_geist_sheet.png | ||
|
||
|
File renamed without changes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,18 @@ | ||
import strfmt | ||
import glm | ||
|
||
type | ||
Rectangle* = object | ||
x*, y*, width*, height*: int | ||
x*, y*, width*, height*: int | ||
|
||
|
||
proc `$`*(self: Rectangle): string {.inline.}= | ||
return "Rectangle(x={0}, y={1}, width={2}, height={3})".fmt(self.x, self.y, self.width, self.height) | ||
|
||
|
||
## Check to see if a point is within a rectangle (inclusive) | ||
proc contains*(r: Rectangle; p: Vec2f): bool {.inline.}= | ||
return p.x >= r.x.float and | ||
p.y >= r.y.float and | ||
p.x <= (r.x + r.width).float and | ||
p.y <= (r.y + r.height).float |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,347 @@ | ||
from strutils import find, strip, isNilOrEmpty, splitWhitespace, split, join, parseInt | ||
from ospaths import parentDir, joinPath | ||
from tables import Table, initTable, contains, len, `[]`, `[]=` | ||
from geom import Rectangle, contains, `$` | ||
from zgl import Texture2D, width, height | ||
from texture import loadTexture | ||
import strfmt | ||
import glm | ||
|
||
# TODO remove Console Logger imports | ||
import logging as log | ||
#from logging import debug, info | ||
|
||
# TODO remove later | ||
log.addHandler(newConsoleLogger()) | ||
|
||
|
||
|
||
# Section from the spritesheet to show | ||
type Frame* = object | ||
name*: string # Name of the Frame | ||
rect*: Rectangle # sub-section of the spritesheet that is one frame | ||
origin*: Vec2f # center (relative to the `rect`) of the Frame, default is (0, 0) | ||
|
||
|
||
# String representation of a Frame | ||
proc `$`*(self: Frame): string= | ||
var s = "Frame[{0}] r={1}".fmt(self.name, $self.rect) | ||
if (self.origin.x) != 0 or (self.origin.y != 0): | ||
s &= " o=({0}, {1})".fmt(self.origin.x, self.origin.y) | ||
|
||
return s | ||
|
||
|
||
|
||
# A Frame with a timing value | ||
type TimedFrame* = object | ||
frame*: Frame # Frame that should be shown | ||
hold*: int # How long to hold the frame out for (in milliseconds), should be positive | ||
|
||
|
||
# String representation of a TimedFrame | ||
proc `$`*(self: TimedFrame): string= | ||
return "TimedFrame[{0}] h={1}".fmt(self.frame.name, self.hold) | ||
|
||
|
||
|
||
# A collection of frames in order | ||
type Sequence* = object | ||
name*: string # Name of the Sequence | ||
frames*: seq[TimedFrame] # Frames that makeup the sequence | ||
looping*: bool # Does the sequence loop? | ||
# TODO cache of total duration? | ||
|
||
|
||
# String representation of a TimedFrame | ||
proc `$`*(self: Sequence): string= | ||
var s = "Sequence[{0}".fmt(self.name) | ||
if self.looping: | ||
s &= ", looping" | ||
s &= "] " | ||
|
||
for tf in self.frames: | ||
s &= "{0}:{1}, ".fmt(tf.frame.name, tf.hold) | ||
s = s[0..(s.len() - 3)] | ||
|
||
return s | ||
|
||
|
||
|
||
type | ||
# The actual sprite object | ||
Sprite* = object of RootObj | ||
visability*: float # [0.0, 1.0], visability of the frame | ||
|
||
# Geometric data | ||
pos*: Vec3f # Location | ||
rotation*: Mat3f # Orientation | ||
scale*: Vec3f # Scale | ||
# TODO non-center orientation origin | ||
|
||
|
||
# TODO Ref object instead?, that way we can use methods on the sprite objects | ||
ZSprite* = object of Sprite | ||
spritesheet: Texture2D # Spritesheet | ||
frames: Table[string, Frame] # hash of the frames (Frame.name -> Frame) | ||
sequences: Table[string, Sequence] # hash of the sequences (Sequence.name -> Sequence) | ||
defaultFrameName: string # Name of the default frame | ||
|
||
|
||
# For parsing state | ||
ZSpriteParsingState = enum | ||
None # Have not started, or done anything | ||
FindingVersionNumber # Looking for the verison number | ||
FindingSpriteSheet # Looking for the spritesheet | ||
FindingInfoBlocks # Looking for a "frame_info" or "sequence_info" block | ||
ReadingFrameInfo # Going through the specified frames | ||
ReadingSequenceInfo # Going through the sequence info | ||
Done # Successfully read through the sprite | ||
|
||
|
||
# List of accepted ZSprite versions | ||
ValidVersions* {.pure.} = enum | ||
None # Null version no. | ||
V10 = "v1.0" # v1.0 | ||
|
||
|
||
# ZSprite accessors | ||
proc spritesheet*(self: ZSprite): Texture2D {.inline.}= | ||
return self.spritesheet | ||
|
||
|
||
# Errors to use for ZSprite parsing | ||
type | ||
InvalidSectionError* = object of ValueError | ||
NonUniqueNameError* = object of ValueError | ||
InvalidEffectError* = object of ValueError | ||
ZSpriteLoadError* = object of ValueError | ||
|
||
|
||
|
||
# Tokens for parsing | ||
const | ||
FrameInfoToken = "frame_info:" | ||
SequenceInfoToken = "sequence_info:" | ||
LoopingToken = "looping" | ||
|
||
|
||
|
||
# Removes comments from a string | ||
proc stripOutComments(s: string): string= | ||
let loc = s.find('#') | ||
if loc == -1: | ||
return s | ||
else: | ||
return s[0..(loc-1)] | ||
|
||
|
||
# Removes all whitespace from a string | ||
proc removeWhitespace(s: string): string {.inline.}= | ||
return splitWhitespace(s).join() | ||
|
||
|
||
# TODO document, what kind of errors this can raise | ||
proc loadZSprite*(zspriteFilename: string): ZSprite = | ||
# Set the initial structure | ||
var sprite = ZSprite( | ||
visability: 1.0, | ||
pos: vec3f(0, 0, 0), | ||
rotation: mat3f(1), # Identity matrix | ||
scale: vec3f(1, 1, 1), | ||
frames: initTable[string, Frame](), | ||
sequences: initTable[string, Sequence]() | ||
) | ||
|
||
# Vars for parsing | ||
var | ||
zspriteFile: File | ||
line = "" | ||
lineCount = 0 | ||
state: ZSpriteParsingState = None | ||
version: ValidVersions | ||
|
||
# Start reading through the file | ||
zspritefile = zspriteFilename.open() | ||
state = FindingVersionNumber | ||
|
||
# Go line by line | ||
while zspriteFile.readline(line): | ||
lineCount += 1 | ||
let cl = strip(stripOutComments(line)) # cleaned line | ||
|
||
# # Print the line | ||
# log.info("{0}: {1}".fmt(lineCount, line)) | ||
|
||
# Skip empty lines | ||
if cl.isNilOrEmpty(): | ||
continue | ||
|
||
# Reading is a bit of a state machine process | ||
case state: | ||
# Look for a valid version number | ||
of FindingVersionNumber: | ||
if cl == $ValidVersions.V10: | ||
version = ValidVersions.V10 | ||
log.info("Detected {0} ZSprite in `{1}`".fmt($version, zspriteFilename)) | ||
|
||
# Move to the next state | ||
state = FindingSpriteSheet | ||
|
||
# Look for the spriteshet (it's relative to the .zsprite file) | ||
of FindingSpriteSheet: | ||
let | ||
spriteDir = parentDir(zspriteFilename) | ||
spritesheetPath = joinPath(spriteDir, cl) | ||
|
||
# Print where the sheet is located at | ||
log.info("Spritesheet is at: " & spritesheetPath) | ||
sprite.spritesheet = loadTexture(spritesheetPath) | ||
|
||
# Shift to finding info | ||
state = FindingInfoBlocks | ||
|
||
# Looking for a frame info (this is a transitionary block) | ||
of FindingInfoBlocks: | ||
if cl == FrameInfoToken: | ||
state = ReadingFrameInfo | ||
else: | ||
raise newException(InvalidSectionError, "Expected `frame_info` block, instead got {0}".fmt(cl)) | ||
|
||
# Read frame info | ||
of ReadingFrameInfo: | ||
# First check for state change | ||
if cl == FrameInfoToken: | ||
continue # Skip line, we're already reading Frame Info | ||
elif cl == SequenceInfoToken: | ||
state = ReadingSequenceInfo # Move to Sequence Info | ||
continue | ||
|
||
# Line must be frame info, nab some data | ||
let | ||
parts = removeWhitespace(cl).split('=') | ||
frameName = parts[0] | ||
frameGeometry = parts[1].split(':') | ||
loc = frameGeometry[0].split(',') | ||
size = frameGeometry[1].split(',') | ||
|
||
# Create the frame | ||
var frame = Frame( | ||
name: frameName, | ||
rect: Rectangle( | ||
x: loc[0].parseInt(), | ||
y: loc[1].parseInt(), | ||
width: size[0].parseInt(), | ||
height: size[1].parseInt() | ||
), | ||
origin: vec2f(0) | ||
) | ||
|
||
# Non (0, 0) origin? | ||
if frameGeometry.len() > 2: | ||
let altOrigin = frameGeometry[2].split(',') | ||
frame.origin.x = altOrigin[0].parseInt().float | ||
frame.origin.y = altOrigin[1].parseInt().float | ||
|
||
# Verify the geometry is good | ||
let geometryGood = | ||
frame.rect.x >= 0 and | ||
frame.rect.y >= 0 and | ||
frame.rect.width <= sprite.spritesheet.width() and | ||
frame.rect.height <= sprite.spritesheet.height() and | ||
frame.rect.contains(frame.origin + vec2f(frame.rect.x.float, frame.rect.y.float)) | ||
if not geometryGood: | ||
raise newException(ValueError, "Invalid Geometry for {0}".fmt($frame)) | ||
|
||
# Make sure the frame isn't in there already | ||
if not (frame.name in sprite.frames): | ||
sprite.frames[frame.name] = frame | ||
|
||
# Is this the first frame that we are reading? | ||
if sprite.defaultFrameName.isNilOrEmpty(): | ||
sprite.defaultFrameName = frame.name | ||
else: | ||
raise newException(NonUniqueNameError, "Frame name `{0}` already exists in the sprite".fmt(frame.name)) | ||
|
||
# Read sequence info | ||
of ReadingSequenceInfo: | ||
# First check for state change | ||
if cl == SequenceInfoToken: | ||
continue # Skip line, we're already reading Sequence Info | ||
elif cl == FrameInfoToken: | ||
state = ReadingSequenceInfo # Move to Frame Info | ||
continue | ||
|
||
# Create the sequence | ||
var sequence = Sequence(looping: false, frames: @[]) | ||
|
||
# Line must be sequence info, nab some data | ||
let | ||
parts = removeWhitespace(cl).split('=') | ||
effectsStart = parts[0].find('[') | ||
effectsEnd = parts[0].find(']') | ||
|
||
# Are there effects? | ||
if (effectsStart != -1) and (effectsEnd != -1): | ||
# parse out the effects | ||
let effects = parts[0][(effectsStart + 1)..(effectsEnd - 1)].split(',') | ||
for e in effects: | ||
# Looping is the only effect right now | ||
if e == LoopingToken: | ||
sequence.looping = true | ||
else: | ||
# Not a known effect | ||
raise newException(InvalidEffectError, "`{0}` is not a valid effect".fmt(e)) | ||
elif (effectsStart != -1) and (effectsEnd != -1): | ||
raise newException(ValueError, "Effects secttion is malformed") | ||
|
||
# Grab the name | ||
if effectsStart == -1: | ||
# No effects, whole thing is the name | ||
sequence.name = parts[0] | ||
else: | ||
# Must start before the effects list | ||
sequence.name = parts[0][0..(effectsStart - 1)] | ||
|
||
# Now parse out the timed frames | ||
let timedFrames = parts[1].split(',') | ||
for tfStr in timedFrames: | ||
let | ||
tfParts = tfStr.split(':') | ||
tf = TimedFrame( | ||
frame: sprite.frames[tfParts[0]], # Get pointer to Frame via name, throw error if name not found | ||
hold: tfparts[1].parseInt() # Get hold value | ||
) | ||
|
||
# Check that the TimeFrame is valid | ||
if tf.hold < 1: | ||
raise newException(ValueError, "Hold value for {0} in {1} must be non-negative".fmt(tf.frame.name, tf.hold)) | ||
|
||
# Else, it's good, add it in to the sequence | ||
sequence.frames.add(tf) | ||
|
||
# Make sure the sequence isn't there either | ||
if not (sequence.name in sprite.sequences): | ||
sprite.sequences[sequence.name] = sequence | ||
else: | ||
raise newException(NonUniqueNameError, "Sequence name `{0}` already exists in the sprite".fmt(sequence.name)) | ||
else: | ||
discard | ||
|
||
# close the file | ||
zspriteFile.close() | ||
|
||
# Last checks | ||
let | ||
atLeastOneFrame = sprite.frames.len() >= 1 | ||
inTerminalState = (state == ReadingFrameInfo) or (state == ReadingSequenceInfo) | ||
if atLeastOneFrame and inTerminalState: | ||
state = Done | ||
|
||
if state != Done: | ||
log.debug("Didn't properly read a file. Failed at state: " & $state) | ||
raise newException(ZSpriteLoadError, "Wasn't able to properly read the ZSprite at `{0}`".fmt(zspriteFilename)) | ||
|
||
log.info("ZSprite with {0} frame(s) and {1} sequence(s) successfully loaded".fmt(sprite.frames.len(), sprite.sequences.len())) | ||
return sprite | ||
|
Oops, something went wrong.