Skip to content

Commit

Permalink
Starting on the sprite implementation
Browse files Browse the repository at this point in the history
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
define-private-public committed Sep 2, 2017
1 parent 7f6ed99 commit 3485f32
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 3 deletions.
Empty file.
1 change: 1 addition & 0 deletions specs/zsprite_examples/BlauGeist.zsprite
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

Expand Down
File renamed without changes
3 changes: 2 additions & 1 deletion src/zengine.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export
zengine.gui,
zengine.models,
zengine.primitives,
# zengine.sprite,
zengine.text,
zengine.texture,
zengine.zgl,
zengine
zengine
2 changes: 1 addition & 1 deletion src/zengine/color.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ const TRANSPARENT* = ZColor(r: 0, g: 0, b: 0, a: 0 )
const BLACK* = ZColor(r: 0, g: 0, b: 0, a: 255)
const GREEN* = ZColor(r: 0, g: 255, b: 0, a: 255)
const BLUE* = ZColor(r: 0, g: 0, b: 255, a: 255)
const GRAY* = ZColor(r: 130, g: 130, b: 130, a: 255)
const GRAY* = ZColor(r: 130, g: 130, b: 130, a: 255)
17 changes: 16 additions & 1 deletion src/zengine/geom.nim
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
347 changes: 347 additions & 0 deletions src/zengine/sprite.nim
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

Loading

0 comments on commit 3485f32

Please sign in to comment.