Fixes #496 and improves memory consumption on render of game_controls (#501)

* Fixes #496 using ebiten.SubImage to Render Section of a Surface.

I had to add matching functions to both animation and sprite to get it to be called
for a sprite object.

* Fixed linter warning on comments for Sprite and Animation.

* Removing what's remaining of the old Sprite re-generation and caching.
This commit is contained in:
Maxime Lavigne (malavv) 2020-06-30 12:43:13 -04:00 committed by GitHub
parent cec12e4138
commit 3990df3bac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 47 deletions

View File

@ -23,6 +23,8 @@ type Surface interface {
PushTranslation(x, y int)
PushBrightness(brightness float64)
Render(surface Surface) error
// Renders a section of the surface enclosed by bounds
RenderSection(surface Surface, bound image.Rectangle) error
ReplacePixels(pixels []byte) error
Screenshot() *image.RGBA
}

View File

@ -2,6 +2,7 @@ package d2asset
import (
"errors"
"image"
"image/color"
"math"
@ -38,6 +39,7 @@ type animationDirection struct {
frames []*animationFrame
}
// Animation has directionality, play modes, and frame counting
type Animation struct {
directions []*animationDirection
frameIndex int
@ -67,6 +69,7 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare
for _, dccFrame := range dccDirection.Frames {
minX, minY := math.MaxInt32, math.MaxInt32
maxX, maxY := math.MinInt32, math.MinInt32
for _, dccFrame := range dccDirection.Frames {
minX = d2common.MinInt(minX, dccFrame.Box.Left)
minY = d2common.MinInt(minY, dccFrame.Box.Top)
@ -78,8 +81,10 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare
frameHeight := maxY - minY
pixels := make([]byte, frameWidth*frameHeight*4)
for y := 0; y < frameHeight; y++ {
for x := 0; x < frameWidth; x++ {
if paletteIndex := dccFrame.PixelData[y*frameWidth+x]; paletteIndex != 0 {
palColor := palette.Colors[paletteIndex]
offset := (x + y*frameWidth) * 4
@ -91,12 +96,12 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare
}
}
image, err := d2render.NewSurface(frameWidth, frameHeight, d2interface.FilterNearest)
sfc, err := d2render.NewSurface(frameWidth, frameHeight, d2interface.FilterNearest)
if err != nil {
return nil, err
}
if err := image.ReplacePixels(pixels); err != nil {
if err := sfc.ReplacePixels(pixels); err != nil {
return nil, err
}
@ -110,7 +115,7 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare
height: dccFrame.Height,
offsetX: minX,
offsetY: minY,
image: image,
image: sfc,
})
}
}
@ -126,7 +131,7 @@ func CreateAnimationFromDC6(dc6 *d2dc6.DC6, palette *d2dat.DATPalette) (*Animati
}
for frameIndex, dc6Frame := range dc6.Frames {
image, err := d2render.NewSurface(int(dc6Frame.Width), int(dc6Frame.Height), d2interface.FilterNearest)
sfc, err := d2render.NewSurface(int(dc6Frame.Width), int(dc6Frame.Height), d2interface.FilterNearest)
if err != nil {
return nil, err
}
@ -166,17 +171,19 @@ func CreateAnimationFromDC6(dc6 *d2dc6.DC6, palette *d2dat.DATPalette) (*Animati
}
colorData := make([]byte, dc6Frame.Width*dc6Frame.Height*4)
for i := 0; i < int(dc6Frame.Width*dc6Frame.Height); i++ {
if indexData[i] < 1 { // TODO: Is this == -1 or < 1?
continue
}
colorData[i*4] = palette.Colors[indexData[i]].R
colorData[i*4+1] = palette.Colors[indexData[i]].G
colorData[i*4+2] = palette.Colors[indexData[i]].B
colorData[i*4+3] = 0xff
}
if err := image.ReplacePixels(colorData); err != nil {
if err := sfc.ReplacePixels(colorData); err != nil {
return nil, err
}
@ -191,7 +198,7 @@ func CreateAnimationFromDC6(dc6 *d2dc6.DC6, palette *d2dat.DATPalette) (*Animati
height: int(dc6Frame.Height),
offsetX: int(dc6Frame.OffsetX),
offsetY: int(dc6Frame.OffsetY),
image: image,
image: sfc,
})
}
@ -279,6 +286,19 @@ func (a *Animation) RenderFromOrigin(target d2interface.Surface) error {
return a.Render(target)
}
// RenderSection renders the section of the animation frame enclosed by bounds
func (a *Animation) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error {
direction := a.directions[a.directionIndex]
frame := direction.frames[a.frameIndex]
sfc.PushTranslation(frame.offsetX, frame.offsetY)
sfc.PushCompositeMode(a.compositeMode)
sfc.PushColor(a.colorMod)
defer sfc.PopN(3)
return sfc.RenderSection(frame.image, bound)
}
// GetFrameSize gets the Size(width, height) of a indexed frame.
func (a *Animation) GetFrameSize(frameIndex int) (int, int, error) {
direction := a.directions[a.directionIndex]
if frameIndex >= len(direction.frames) {
@ -289,11 +309,13 @@ func (a *Animation) GetFrameSize(frameIndex int) (int, int, error) {
return frame.width, frame.height, nil
}
// GetCurrentFrameSize gets the Size(width, height) of the current frame.
func (a *Animation) GetCurrentFrameSize() (int, int) {
width, height, _ := a.GetFrameSize(a.frameIndex)
return width, height
}
// GetFrameBounds gets maximum Size(width, height) of all frame.
func (a *Animation) GetFrameBounds() (int, int) {
maxWidth, maxHeight := 0, 0
@ -306,27 +328,33 @@ func (a *Animation) GetFrameBounds() (int, int) {
return maxWidth, maxHeight
}
// GetCurrentFrame gets index of current frame in animation
func (a *Animation) GetCurrentFrame() int {
return a.frameIndex
}
// GetFrameCount gets number of frames in animation
func (a *Animation) GetFrameCount() int {
direction := a.directions[a.directionIndex]
return len(direction.frames)
}
// IsOnFirstFrame gets if the animation on its first frame
func (a *Animation) IsOnFirstFrame() bool {
return a.frameIndex == 0
}
// IsOnLastFrame gets if the animation on its last frame
func (a *Animation) IsOnLastFrame() bool {
return a.frameIndex == a.GetFrameCount()-1
}
// GetDirectionCount gets the number of animation direction
func (a *Animation) GetDirectionCount() int {
return len(a.directions)
}
// SetDirection places the animation in the direction of an animation
func (a *Animation) SetDirection(directionIndex int) error {
if directionIndex >= 64 {
return errors.New("invalid direction index")
@ -337,10 +365,12 @@ func (a *Animation) SetDirection(directionIndex int) error {
return nil
}
// GetDirection get the current animation direction
func (a *Animation) GetDirection() int {
return a.directionIndex
}
// SetCurrentFrame sets animation at a specific frame
func (a *Animation) SetCurrentFrame(frameIndex int) error {
if frameIndex >= a.GetFrameCount() {
return errors.New("invalid frame index")
@ -351,29 +381,35 @@ func (a *Animation) SetCurrentFrame(frameIndex int) error {
return nil
}
// Rewind animation to beginning
func (a *Animation) Rewind() {
a.SetCurrentFrame(0)
}
// PlayForward plays animation forward
func (a *Animation) PlayForward() {
a.playMode = playModeForward
a.lastFrameTime = 0
}
// PlayBackward plays animation backward
func (a *Animation) PlayBackward() {
a.playMode = playModeBackward
a.lastFrameTime = 0
}
// Pause animation
func (a *Animation) Pause() {
a.playMode = playModePause
a.lastFrameTime = 0
}
// SetPlayLoop sets whether to loop the animation
func (a *Animation) SetPlayLoop(loop bool) {
a.playLoop = loop
}
// SetPlaySpeed sets play speed of the animation
func (a *Animation) SetPlaySpeed(playSpeed float64) {
a.SetPlayLength(playSpeed * float64(a.GetFrameCount()))
}
@ -391,14 +427,17 @@ func (a *Animation) SetColorMod(color color.Color) {
a.colorMod = color
}
// GetPlayedCount gets the number of times the application played
func (a *Animation) GetPlayedCount() int {
return a.playedCount
}
// ResetPlayedCount resets the play count
func (a *Animation) ResetPlayedCount() {
a.playedCount = 0
}
// SetBlend sets the animation alpha blending status
func (a *Animation) SetBlend(blend bool) {
if blend {
a.compositeMode = d2enum.CompositeModeLighter

View File

@ -76,6 +76,22 @@ func (s *ebitenSurface) Render(sfc d2interface.Surface) error {
return s.image.DrawImage(img, opts)
}
// Renders the section of the animation frame enclosed by bounds
func (s *ebitenSurface) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error {
opts := &ebiten.DrawImageOptions{CompositeMode: s.stateCurrent.mode}
opts.GeoM.Translate(float64(s.stateCurrent.x), float64(s.stateCurrent.y))
opts.Filter = s.stateCurrent.filter
if s.stateCurrent.color != nil {
opts.ColorM = ColorToColorM(s.stateCurrent.color)
}
if s.stateCurrent.brightness != 0 {
opts.ColorM.ChangeHSV(0, 1, s.stateCurrent.brightness)
}
var img = sfc.(*ebitenSurface).image
return s.image.DrawImage(img.SubImage(bound).(*ebiten.Image), opts)
}
func (s *ebitenSurface) DrawText(format string, params ...interface{}) {
ebitenutil.DebugPrintAt(s.image, fmt.Sprintf(format, params...), s.stateCurrent.x, s.stateCurrent.y)
}

View File

@ -2,6 +2,7 @@ package d2ui
import (
"errors"
"image"
"image/color"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -10,6 +11,7 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
)
// Sprite is a positioned visual object.
type Sprite struct {
x int
y int
@ -32,14 +34,25 @@ func (s *Sprite) Render(target d2interface.Surface) error {
target.PushTranslation(s.x, s.y-frameHeight)
defer target.Pop()
return s.animation.Render(target)
}
// RenderSection renders the section of the sprite enclosed by bounds
func (s *Sprite) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error {
sfc.PushTranslation(s.x, s.y-bound.Dy())
defer sfc.Pop()
return s.animation.RenderSection(sfc, bound)
}
func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segmentsY, frameOffset int) error {
var currentY int
for y := 0; y < segmentsY; y++ {
var currentX int
var maxFrameHeight int
for x := 0; x < segmentsX; x++ {
if err := s.animation.SetCurrentFrame(x + y*segmentsX + frameOffset*segmentsX*segmentsY); err != nil {
return err
@ -48,6 +61,7 @@ func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segments
target.PushTranslation(s.x+currentX, s.y+currentY)
err := s.animation.Render(target)
target.Pop()
if err != nil {
return err
}
@ -63,75 +77,93 @@ func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segments
return nil
}
// SetPosition places the sprite in 2D
func (s *Sprite) SetPosition(x, y int) {
s.x = x
s.y = y
}
// GetPosition retrieves the 2D position of the sprite
func (s *Sprite) GetPosition() (int, int) {
return s.x, s.y
}
// GetFrameSize gets the Size(width, height) of a indexed frame.
func (s *Sprite) GetFrameSize(frameIndex int) (int, int, error) {
return s.animation.GetFrameSize(frameIndex)
}
// GetCurrentFrameSize gets the Size(width, height) of the current frame.
func (s *Sprite) GetCurrentFrameSize() (int, int) {
return s.animation.GetCurrentFrameSize()
}
// GetFrameBounds gets maximum Size(width, height) of all frame.
func (s *Sprite) GetFrameBounds() (int, int) {
return s.animation.GetFrameBounds()
}
// GetCurrentFrame gets index of current frame in animation
func (s *Sprite) GetCurrentFrame() int {
return s.animation.GetCurrentFrame()
}
// GetFrameCount gets number of frames in animation
func (s *Sprite) GetFrameCount() int {
return s.animation.GetFrameCount()
}
// IsOnFirstFrame gets if the animation on its first frame
func (s *Sprite) IsOnFirstFrame() bool {
return s.animation.IsOnFirstFrame()
}
// IsOnLastFrame gets if the animation on its last frame
func (s *Sprite) IsOnLastFrame() bool {
return s.animation.IsOnLastFrame()
}
// GetDirectionCount gets the number of animation direction
func (s *Sprite) GetDirectionCount() int {
return s.animation.GetDirectionCount()
}
// SetDirection places the animation in the direction of an animation
func (s *Sprite) SetDirection(directionIndex int) error {
return s.animation.SetDirection(directionIndex)
}
// GetDirection get the current animation direction
func (s *Sprite) GetDirection() int {
return s.animation.GetDirection()
}
// SetCurrentFrame sets animation at a specific frame
func (s *Sprite) SetCurrentFrame(frameIndex int) error {
return s.animation.SetCurrentFrame(frameIndex)
}
// Rewind sprite to beginning
func (s *Sprite) Rewind() {
s.animation.SetCurrentFrame(0)
}
// PlayForward plays sprite forward
func (s *Sprite) PlayForward() {
s.animation.PlayForward()
}
// PlayBackward play sprites backward
func (s *Sprite) PlayBackward() {
s.animation.PlayBackward()
}
// Pause animation
func (s *Sprite) Pause() {
s.animation.Pause()
}
// SetPlayLoop sets whether to loop the animation
func (s *Sprite) SetPlayLoop(loop bool) {
s.animation.SetPlayLoop(loop)
}
@ -148,6 +180,7 @@ func (s *Sprite) SetColorMod(color color.Color) {
s.animation.SetColorMod(color)
}
// SetBlend sets the animation alpha blending status
func (s *Sprite) SetBlend(blend bool) {
s.animation.SetBlend(blend)
}

View File

@ -1,6 +1,7 @@
package d2player
import (
"image"
"image/color"
"log"
"math"
@ -16,7 +17,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
@ -48,14 +48,10 @@ type GameControls struct {
FreeCam bool
lastMouseX int
lastMouseY int
lastHealthPercent float64
lastManaPercent float64
// UI
globeSprite *d2ui.Sprite
hpManaStatusSprite *d2ui.Sprite
hpStatusBar d2interface.Surface
manaStatusBar d2interface.Surface
mainPanel *d2ui.Sprite
menuButton *d2ui.Sprite
skillIcon *d2ui.Sprite
@ -256,8 +252,6 @@ func (g *GameControls) Load() {
animation, _ = d2asset.LoadAnimation(d2resource.HealthManaIndicator, d2resource.PaletteSky)
g.hpManaStatusSprite, _ = d2ui.LoadSprite(animation)
g.hpStatusBar, _ = d2render.NewSurface(globeWidth, globeHeight, d2interface.FilterNearest)
animation, _ = d2asset.LoadAnimation(d2resource.GamePanels, d2resource.PaletteSky)
g.mainPanel, _ = d2ui.LoadSprite(animation)
@ -369,28 +363,18 @@ func (g *GameControls) Render(target d2interface.Surface) {
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
// Health status bar
healthPercent := float64(g.hero.Stats.Health) / float64(g.hero.Stats.MaxHealth)
hpBarHeight := int(healthPercent * float64(globeHeight))
g.hpManaStatusSprite.SetCurrentFrame(0)
g.hpManaStatusSprite.SetPosition(offset+30, height-13)
g.hpManaStatusSprite.RenderSection(target, image.Rect(0, globeHeight-hpBarHeight, globeWidth, globeHeight))
// Left globe
g.globeSprite.SetCurrentFrame(0)
g.globeSprite.SetPosition(offset+28, height-5)
g.globeSprite.Render(target)
// Health status bar
healthPercent := float64(g.hero.Stats.Health) / float64(g.hero.Stats.MaxHealth)
hpBarHeight := int(healthPercent * float64(globeHeight))
if g.lastHealthPercent != healthPercent {
g.hpStatusBar, _ = d2render.NewSurface(globeWidth, hpBarHeight, d2interface.FilterNearest)
g.hpManaStatusSprite.SetCurrentFrame(0)
g.hpStatusBar.PushTranslation(0, hpBarHeight)
g.hpManaStatusSprite.Render(g.hpStatusBar)
g.hpStatusBar.Pop()
g.lastHealthPercent = healthPercent
}
target.PushTranslation(30, 508+(globeHeight-hpBarHeight))
target.Render(g.hpStatusBar)
target.Pop()
offset += w
// Left skill
@ -460,29 +444,19 @@ func (g *GameControls) Render(target d2interface.Surface) {
g.mainPanel.SetPosition(offset, height)
g.mainPanel.Render(target)
// Mana status bar
manaPercent := float64(g.hero.Stats.Mana) / float64(g.hero.Stats.MaxMana)
manaBarHeight := int(manaPercent * float64(globeHeight))
g.hpManaStatusSprite.SetCurrentFrame(1)
g.hpManaStatusSprite.SetPosition(offset+7, height-12)
g.hpManaStatusSprite.RenderSection(target, image.Rect(0, globeHeight-manaBarHeight, globeWidth, globeHeight))
// Right globe
g.globeSprite.SetCurrentFrame(1)
g.globeSprite.SetPosition(offset+8, height-8)
g.globeSprite.Render(target)
g.globeSprite.Render(target)
// Mana status bar
manaPercent := float64(g.hero.Stats.Mana) / float64(g.hero.Stats.MaxMana)
manaBarHeight := int(manaPercent * float64(globeHeight))
if manaPercent != g.lastManaPercent {
g.manaStatusBar, _ = d2render.NewSurface(globeWidth, manaBarHeight, d2interface.FilterNearest)
g.hpManaStatusSprite.SetCurrentFrame(1)
g.manaStatusBar.PushTranslation(0, manaBarHeight)
g.hpManaStatusSprite.Render(g.manaStatusBar)
g.manaStatusBar.Pop()
g.lastManaPercent = manaPercent
}
target.PushTranslation(offset+8, 508+(globeHeight-manaBarHeight))
target.Render(g.manaStatusBar)
target.Pop()
if g.isZoneTextShown {
g.zoneChangeText.SetPosition(width/2, height/4)
g.zoneChangeText.Render(target)