непонятно как делать animator

This commit is contained in:
DBotThePony 2023-02-13 15:42:42 +07:00
parent ed12e99d43
commit 0052adf89a
Signed by: DBot
GPG Key ID: DCC23B5715498507
17 changed files with 266 additions and 282 deletions

View File

@ -44,6 +44,7 @@ import ru.dbotthepony.kstarbound.io.json.AABBiTypeAdapter
import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter
import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.io.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.io.json.NothingAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2fTypeAdapter
import ru.dbotthepony.kstarbound.io.json.Vector2iTypeAdapter
@ -110,6 +111,8 @@ class Starbound : ISBFileLocator {
registerTypeAdapter(InternedStringAdapter(stringInterner))
registerTypeAdapter(InternedJsonElementAdapter(stringInterner))
registerTypeAdapter(Nothing::class.java, NothingAdapter)
// Обработчик @JsonImplementation
registerTypeAdapterFactory(JsonImplementationTypeFactory)
@ -387,7 +390,7 @@ class Starbound : ISBFileLocator {
loadStage(callback, _particles, ext2files["particle"] ?: listOf())
pathStack.block("/") {
playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java)
//playerDefinition = gson.fromJson(locate("/player.config").reader(), PlayerDefinition::class.java)
}
initializing = false

View File

@ -11,8 +11,8 @@ import ru.dbotthepony.kstarbound.client.gl.vertex.quad
import ru.dbotthepony.kstarbound.client.gl.vertex.shadowLine
import ru.dbotthepony.kstarbound.client.render.ConfiguredStaticMesh
import ru.dbotthepony.kstarbound.client.render.entity.EntityRenderer
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
import ru.dbotthepony.kstarbound.client.render.GPULightRenderer
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TileLayerList
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.world.*
@ -166,22 +166,6 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
backgroundRenderer.loadRenderers(getBackgroundView())
}
/**
* Отрисовывает всю геометрию напрямую
*/
fun render(stack: Matrix4fStack) {
backgroundRenderer.render(stack)
foregroundRenderer.render(stack)
}
/**
* Отрисовывает всю геометрию напрямую, с проверкой, изменился ли чанк
*/
fun bakeAndRender(stack: Matrix4fStack) {
backgroundRenderer.bakeAndRender(stack, this::getBackgroundView)
foregroundRenderer.bakeAndRender(stack, this::getForegroundView)
}
/**
* Тесселирует "статичную" геометрию в builders (к примеру тайлы), с проверкой, изменилось ли что либо,
* и загружает её в видеопамять.
@ -293,15 +277,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
}
}
/**
* Хранит состояние отрисовки этого чанка
*
* Должен быть использован только один раз, после выкинут, иначе поведение
* кода невозможно будет предсказать
*/
inner class OneShotRenderer constructor(val origin: ChunkPos = pos) : ILayeredRenderer, GPULightRenderer.ShadowGeometryRenderer {
private val layerQueue = ArrayDeque<Pair<(Matrix4fStack) -> Unit, Int>>()
inner class Renderer(val renderOrigin: ChunkPos = pos) : GPULightRenderer.ShadowGeometryRenderer {
override fun renderHardGeometry(
renderer: GPULightRenderer,
lightPosition: Vector2f,
@ -309,7 +285,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
stack: Matrix4fStack,
program: GLHardLightGeometryProgram
) {
if (!intersectCircleRectangle(lightPosition, lightRadius, origin.x * CHUNK_SIZEf, origin.y * CHUNK_SIZEf, (origin.x + 1) * CHUNK_SIZEf, (origin.y + 1) * CHUNK_SIZEf)) {
if (!intersectCircleRectangle(lightPosition, lightRadius, renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf, (renderOrigin.x + 1) * CHUNK_SIZEf, (renderOrigin.y + 1) * CHUNK_SIZEf)) {
return
}
@ -319,16 +295,16 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
if (intersectCircleRectangle(
lightPosition,
lightRadius,
origin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
renderOrigin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
renderOrigin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
renderOrigin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
renderOrigin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
) {
if (!setOnce) {
program.localToWorldTransform.set(
Matrix4f.IDENTITY.translateWithMultiplication(
Vector3f(x = origin.x * CHUNK_SIZEf,
y = origin.y * CHUNK_SIZEf)))
Vector3f(x = renderOrigin.x * CHUNK_SIZEf,
y = renderOrigin.y * CHUNK_SIZEf)))
setOnce = true
}
@ -345,7 +321,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
stack: Matrix4fStack,
program: GLSoftLightGeometryProgram
) {
if (!intersectCircleRectangle(lightPosition, lightRadius, origin.x * CHUNK_SIZEf, origin.y * CHUNK_SIZEf, (origin.x + 1) * CHUNK_SIZEf, (origin.y + 1) * CHUNK_SIZEf)) {
if (!intersectCircleRectangle(lightPosition, lightRadius, renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf, (renderOrigin.x + 1) * CHUNK_SIZEf, (renderOrigin.y + 1) * CHUNK_SIZEf)) {
return
}
@ -353,18 +329,18 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
for (geometry in shadowGeometry) {
if (intersectCircleRectangle(
lightPosition,
lightRadius,
origin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
origin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
lightPosition,
lightRadius,
renderOrigin.x * CHUNK_SIZEf + geometry.x * SHADOW_GEOMETRY_SQUARE_SIZE,
renderOrigin.y * CHUNK_SIZEf + geometry.y * SHADOW_GEOMETRY_SQUARE_SIZE,
renderOrigin.x * CHUNK_SIZEf + (geometry.x + 1) * SHADOW_GEOMETRY_SQUARE_SIZE,
renderOrigin.y * CHUNK_SIZEf + (geometry.y + 1) * SHADOW_GEOMETRY_SQUARE_SIZE)
) {
if (!setOnce) {
program.localToWorldTransform.set(
Matrix4f.IDENTITY.translateWithMultiplication(
Vector3f(x = origin.x * CHUNK_SIZEf,
y = origin.y * CHUNK_SIZEf)))
Vector3f(x = renderOrigin.x * CHUNK_SIZEf,
y = renderOrigin.y * CHUNK_SIZEf)))
setOnce = true
}
@ -374,26 +350,34 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
}
}
init {
fun addLayers(layers: LayeredRenderer) {
for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
layerQueue.add(baked::renderStacked to (zLevel + Z_LEVEL_BACKGROUND))
layers.add(zLevel + Z_LEVEL_BACKGROUND) {
it.push().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf)
baked.renderStacked(it)
it.pop()
}
}
for ((baked, zLevel) in foregroundRenderer.bakedMeshes) {
layerQueue.add(baked::renderStacked to zLevel)
layers.add(zLevel) {
it.push().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf)
baked.renderStacked(it)
it.pop()
}
}
for (renderer in entityRenderers.values) {
layerQueue.add(lambda@{ it: Matrix4fStack ->
layers.add(renderer.layer) {
val relative = renderer.renderPos - posVector2d
it.push().translateWithMultiplication(relative.x.toFloat(), relative.y.toFloat())
it.push().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf + relative.x.toFloat(), renderOrigin.y * CHUNK_SIZEf + relative.y.toFloat())
renderer.render(it)
it.pop()
Unit
} to renderer.layer)
}
}
layerQueue.add({ it: Matrix4fStack ->
layers.add(Z_LEVEL_LIQUID) {
it.push().translateWithMultiplication(renderOrigin.x * CHUNK_SIZEf, renderOrigin.y * CHUNK_SIZEf)
val types = ArrayList<LiquidDefinition>()
for (x in 0 until CHUNK_SIZE) {
@ -431,44 +415,10 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
builder.upload()
builder.draw()
}
} to Z_LEVEL_LIQUID)
layerQueue.sortBy {
return@sortBy it.second
it.pop()
}
}
override fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int {
if (layerQueue.isEmpty())
return Int.MIN_VALUE
stack.push().translateWithMultiplication(x = origin.x * CHUNK_SIZEf, y = origin.y * CHUNK_SIZEf)
var pair = layerQueue.last()
while (pair.second >= zPos) {
pair.first.invoke(stack)
layerQueue.removeLast()
if (layerQueue.isEmpty()) {
stack.pop()
return Int.MIN_VALUE
}
pair = layerQueue.last()
}
stack.pop()
return layerQueue.last().second
}
override fun bottomMostZLevel(): Int {
if (layerQueue.isEmpty()) {
return Int.MIN_VALUE
}
return layerQueue.last().second
}
}
private val entityRenderers = HashMap<Entity, EntityRenderer>()

View File

@ -1,13 +1,7 @@
package ru.dbotthepony.kstarbound.client
import org.lwjgl.opengl.GL46.*
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.gl.vertex.QuadTransformers
import ru.dbotthepony.kstarbound.client.gl.vertex.quadZ
import ru.dbotthepony.kstarbound.client.render.ILayeredRenderer
import ru.dbotthepony.kstarbound.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB
import ru.dbotthepony.kstarbound.util.DoubleEdgeProgression
import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.util2d.AABB
@ -38,18 +32,17 @@ class ClientWorld(
size: AABB,
isScreenspaceRender: Boolean = true
) {
val determineRenderers = ArrayList<ILayeredRenderer>()
val layers = LayeredRenderer()
client.lightRenderer.begin()
for (chunk in collectPositionAware(size.encasingChunkPosAABB())) {
val renderer = chunk.second.OneShotRenderer(chunk.first)
determineRenderers.add(renderer)
val renderer = chunk.second.Renderer(chunk.first)
renderer.addLayers(layers)
chunk.second.bake()
//client.lightRenderer.addShadowGeometry(renderer)
}
renderLayeredList(client.gl.matrixStack, determineRenderers)
layers.render(client.gl.matrixStack)
/*
for ((lightPosition, color) in listOf(
(client.screenToWorld(client.mouseCoordinatesF)) to Color.RED,

View File

@ -18,6 +18,8 @@ import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.GPULightRenderer
import ru.dbotthepony.kstarbound.client.render.TextAlignY
import ru.dbotthepony.kstarbound.client.render.TileRenderers
import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.util.PausableTimeSource
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kvector.matrix.nfloat.Matrix4f
import ru.dbotthepony.kvector.util2d.AABB
@ -25,6 +27,7 @@ import ru.dbotthepony.kvector.vector.Color
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nfloat.Vector2f
import ru.dbotthepony.kvector.vector.nfloat.Vector3f
import java.io.Closeable
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*
@ -32,7 +35,8 @@ import java.util.concurrent.locks.LockSupport
import kotlin.collections.ArrayList
import kotlin.math.roundToInt
class StarboundClient(val starbound: Starbound) : AutoCloseable {
class StarboundClient(val starbound: Starbound) : Closeable {
val time = PausableTimeSource(JVMTimeSource.INSTANCE)
val window: Long
val camera = Camera(this)
val input = UserInput()

View File

@ -4,6 +4,10 @@ import org.lwjgl.opengl.GL46
import ru.dbotthepony.kstarbound.client.gl.VertexBufferObject
import ru.dbotthepony.kstarbound.util.ByteBufferOutputStream
/**
* Создаёт буфер для данных вне кучи, записывает данные напрямую в него,
* при [upload] загружает данные из буфера напрямую в память видеокарты.
*/
open class DirectVertexBuilder<T : DirectVertexBuilder<T>>(
attributes: GLAttributeList,
type: GeometryType,

View File

@ -1,7 +1,9 @@
package ru.dbotthepony.kstarbound.client.gl.vertex
import org.lwjgl.opengl.GL46
import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.client.gl.checkForGLError
import java.io.Closeable
@ -48,4 +50,21 @@ class StreamVertexBuilder(
vbo.close()
ebo.close()
}
fun singleSprite(x: Float, y: Float, width: Float, height: Float, z: Float = 5f, angle: Double = 0.0, transformer: QuadVertexTransformer) {
begin()
quadRotatedZ(x, y, width, height, z, 0f, 0f, angle, transformer)
upload()
draw()
}
fun singleSprite(width: Float, height: Float, z: Float = 5f, angle: Double = 0.0, transformer: QuadVertexTransformer) {
singleSprite(-width / 2f, -height / 2f, width / 2f, height / 2f, z, angle, transformer)
}
fun singleSprite(width: Float, height: Float, angle: Double = 0.0, transformer: QuadVertexTransformer) {
singleSprite(-width / 2f, -height / 2f, width / 2f, height / 2f, 0f, angle, transformer)
}
}

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kstarbound.client.ClientWorld
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kvector.matrix.Matrix4fStack
class Animator(
val world: ClientWorld,
val def: AnimationDefinition
) {
val frameAnimator: FrameAnimator?
init {
if (def.frames != null && def.animationCycle != null && def.frameNumber != null) {
frameAnimator = FrameAnimator(lastFrame = def.frameNumber - 1, time = world.client.time, animationCycle = def.animationCycle)
} else {
frameAnimator = null
}
}
fun render(stack: Matrix4fStack) {
}
}

View File

@ -1,20 +1,21 @@
package ru.dbotthepony.kstarbound.client.render
import org.lwjgl.glfw.GLFW.glfwGetTime
import ru.dbotthepony.kstarbound.util.ITimeSource
import ru.dbotthepony.kstarbound.util.JVMTimeSource
/**
* Таймер для анимирования набора спрайтов
*/
open class FrameAnimator(
class FrameAnimator(
/**
* Первый кадр в анимации
*/
var firstFrame: Int = 0,
val firstFrame: Int = 0,
/**
* Последний кадр в анимации
*/
var lastFrame: Int,
val lastFrame: Int,
/**
* Сколько времени занимает один кадр
@ -25,38 +26,31 @@ open class FrameAnimator(
* Зациклить ли анимацию
*/
var animationLoops: Boolean = true,
val time: ITimeSource = JVMTimeSource.INSTANCE
) {
var frame = 0
private set
/**
* Возвращает разницу между последним и первым кадром анимации
*/
val frameDiff get() = lastFrame - firstFrame
private val initial = glfwGetTime()
private var lastRender = initial
/**
* Сколько времени прошло с момента последнего кадра
*/
val delta get() = glfwGetTime() - lastRender
private var counter = 0.0
private var lastRender = time.seconds
/**
* Проверяет glfw таймер и продвигает фрейм анимации
* Разница между последним и первым кадром анимации
*/
fun advance() {
val frameDiff = lastFrame - firstFrame
/**
* Проверяет таймер на [seconds] и продвигает фрейм анимации
*/
fun advance(seconds: Double) {
if (frameDiff == 0)
return
if (frame + firstFrame >= lastFrame && !animationLoops) {
if (frame + firstFrame >= lastFrame && !animationLoops)
return
}
counter += delta / animationCycle
lastRender = glfwGetTime()
counter += seconds / animationCycle
if (counter >= 1.0) {
val desired = frame + counter.toInt()
@ -69,4 +63,14 @@ open class FrameAnimator(
}
}
}
/**
* Проверяет таймер используя [time] и продвигает фрейм анимации
*/
fun advance() {
if (frameDiff != 0) {
advance(time.seconds - lastRender)
lastRender = time.seconds
}
}
}

View File

@ -1,76 +0,0 @@
package ru.dbotthepony.kstarbound.client.render
import ru.dbotthepony.kvector.matrix.Matrix4fStack
/**
* Интерфейс для отрисовки комплексных объектов, в которых множество слоёв, который
* определяет лишь один метод: [renderLayer]
*
* Используется вместе с другими [ILayeredRenderer], где необходимо отрисовывать комплексную сцену,
* реализуя ручную сортировку геометрии.
*/
interface ILayeredRenderer {
/**
* Главный метод отрисовки данной стопки слоёв. Вызов данного метода может что-либо
* отрисовать, а может вообще ничего не отрисовать.
*
* zLevel всегда положителен, и указывает на то, какой слой (или все слои за) надо отрисовать,
* т.е. при вызове этого метода отрисовываются все слои, у которых z позиция >= [zPos]
*
* Возвращается zNew следующего слоя (такой, что [zPos] > zNew).
*
* Если следующего слоя нет, вернуть [Int.MIN_VALUE], и данный объект
* будет считаться отрисованным.
*/
fun renderLayerFromStack(zPos: Int, stack: Matrix4fStack): Int
/**
* Возвращает наибольшее zPos в данной стопке.
*
* Если стопка пуста, то необходимо вернуть [Int.MIN_VALUE].
*
* В зависимости от сцены, которую необходимо отрисовать,
* [renderLayerFromStack] может быть вызван сразу с этим же значением,
* если этот объект имеет самый дальний слой
*/
fun bottomMostZLevel(): Int
}
fun renderLayeredList(transform: Matrix4fStack, potentialRenderers: List<ILayeredRenderer>): Int {
val renderers = ArrayList<ILayeredRenderer>(potentialRenderers.size)
var bottomMost = Int.MIN_VALUE
for (render in potentialRenderers) {
val zLevel = render.bottomMostZLevel()
if (zLevel > Int.MIN_VALUE) {
bottomMost = bottomMost.coerceAtLeast(zLevel)
renderers.add(render)
}
}
var lastBottom = bottomMost
var renderCalls = 0
while (lastBottom != Int.MIN_VALUE && renderers.isNotEmpty()) {
var newBottom = Int.MIN_VALUE
for (i in renderers.size - 1 downTo 0) {
val renderer = renderers[i]
val newLevel = renderer.renderLayerFromStack(lastBottom, transform)
renderCalls++
if (newLevel == Int.MIN_VALUE) {
renderers.removeAt(i)
} else {
newBottom = newBottom.coerceAtLeast(newLevel)
}
}
lastBottom = newBottom
}
return renderCalls
}

View File

@ -0,0 +1,30 @@
package ru.dbotthepony.kstarbound.client.render
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import ru.dbotthepony.kvector.matrix.Matrix4fStack
/**
* Позволяет вызывать отрисовщики в определённой (послойной) последовательности
*/
class LayeredRenderer {
private val layers = Int2ObjectAVLTreeMap<ArrayList<(Matrix4fStack) -> Unit>>()
/**
* Сортировка [layer] происходит от дальнего (БОЛЬШЕ!) к ближнему (МЕНЬШЕ!)
*
* Пример:
* `8 -> 6 -> 4 -> 1 -> -4 -> -7`
*/
fun add(layer: Int, renderer: (Matrix4fStack) -> Unit) {
layers.computeIfAbsent(-layer, Int2ObjectFunction { ArrayList() }).add(renderer)
}
fun render(stack: Matrix4fStack) {
for (list in layers.values) {
for (renderer in list) {
renderer.invoke(stack)
}
}
}
}

View File

@ -1,56 +0,0 @@
package ru.dbotthepony.kstarbound.client.render
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.defs.image.AtlasConfiguration
import ru.dbotthepony.kstarbound.defs.image.ImageReference
class SpriteAnimator(
val sprites: List<BoundSprite>,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = sprites.size - 1
) : FrameAnimator(
firstFrame = firstFrame,
lastFrame = lastFrame,
animationCycle = animationCycle,
animationLoops = animationLoops,
) {
constructor(
image: GLTexture2D,
atlas: AtlasConfiguration,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = atlas.spriteList.size - 1
) : this(
atlas.spriteList.stream().map { it.bind(image) }.collect(ImmutableList.toImmutableList()),
animationCycle = animationCycle,
animationLoops = animationLoops,
firstFrame = firstFrame,
lastFrame = lastFrame,
)
constructor(
image: ImageReference,
state: GLStateTracker,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = image.config.spriteList.size - 1
) : this(state.loadNamedTextureSafe(image.image.fullPath), image.config, animationCycle, animationLoops, firstFrame, lastFrame)
val sprite get() = sprites[frame]
}
fun ImageReference.makeSpriteAnimator(
state: GLStateTracker,
animationCycle: Double,
animationLoops: Boolean = true,
firstFrame: Int = 0,
lastFrame: Int = config.spriteList.size - 1
): SpriteAnimator {
return SpriteAnimator(this, state, animationCycle, animationLoops, firstFrame, lastFrame)
}

View File

@ -25,17 +25,7 @@ class ItemRenderer(state: GLStateTracker, entity: ItemEntity, chunk: ClientChunk
for (texture in textures) {
texture.bind()
val builder = state.flat2DTexturedQuads.small
builder.begin()
val width = (texture.width / PIXELS_IN_STARBOUND_UNITf) / 2f
val height = (texture.height / PIXELS_IN_STARBOUND_UNITf) / 2f
builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, texture.transformer)
builder.upload()
builder.draw()
state.flat2DTexturedQuads.small.singleSprite(texture.width / PIXELS_IN_STARBOUND_UNITf, texture.height / PIXELS_IN_STARBOUND_UNITf, entity.movement.angle, texture.transformer)
}
}
}

View File

@ -12,13 +12,14 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d
@JsonFactory
data class AnimationDefinition(
val frames: ImageReference? = null,
val animatedParts: AnimatedParts? = null,
val variants: Int? = null,
val frameNumber: Int? = null,
val animationCycle: Double? = null,
val offset: Vector2d? = null,
val sounds: ImmutableMap<String, Either<ImmutableList<String>, CustomSound>> = ImmutableMap.of(),
val animatedParts: AnimatedParts? = null,
val sounds: ImmutableMap<String, ImmutableList<String>> = ImmutableMap.of(),
val transformationGroups: ImmutableMap<String, TransformConfig> = ImmutableMap.of(),
val particleEmitters: ImmutableMap<String, ParticleEmitter> = ImmutableMap.of(),
) {
@ -27,12 +28,6 @@ data class AnimationDefinition(
val interpolated: Boolean? = null
)
// TODO
@JsonFactory
data class CustomSound(
val sound: String? = null
)
@JsonFactory
data class AnimatedParts(
val stateTypes: ImmutableMap<String, StateType> = ImmutableMap.of(),
@ -41,6 +36,7 @@ data class AnimationDefinition(
@JsonFactory
data class StateType(
val default: String,
val priority: Int = 0,
val states: ImmutableMap<String, State> = ImmutableMap.of(),
) {
@JsonFactory

View File

@ -9,6 +9,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import ru.dbotthepony.kstarbound.api.ISBFileLocator
import ru.dbotthepony.kstarbound.client.gl.GLTexture2D
import ru.dbotthepony.kstarbound.io.json.stream
@ -18,7 +19,7 @@ import ru.dbotthepony.kvector.vector.nint.Vector4i
import java.util.concurrent.ConcurrentHashMap
/**
* Атлас спрайтов, собранный вручную артистом
* Конфигурация атласа спрайтов, собранный вручную артистом
*
* В файлах игры именуется frames
*
@ -45,10 +46,26 @@ class AtlasConfiguration private constructor(
*/
val first: Sprite = sprites[sprites.keys.stream().sorted().findFirst().orElseThrow { NoSuchElementException("No a single key present in $name") }] ?: throw NoSuchElementException("IMPOSSIBRU in $name")
private val intIndexed = Int2ObjectOpenHashMap<Sprite>()
init {
for ((k, v) in sprites.entries) {
val int = k.toIntOrNull()
if (int != null) {
intIndexed[int] = v
}
}
}
operator fun get(name: String): Sprite? {
return sprites[name]
}
operator fun get(name: Int): Sprite? {
return intIndexed[name]
}
fun any(name: String): Sprite {
return get(name) ?: first
}

View File

@ -0,0 +1,16 @@
package ru.dbotthepony.kstarbound.io.json
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
object NothingAdapter : TypeAdapter<Nothing>() {
override fun write(out: JsonWriter, value: Nothing?) {
out.nullValue()
}
override fun read(`in`: JsonReader): Nothing? {
`in`.skipValue()
return null
}
}

View File

@ -52,6 +52,9 @@ class SBPattern private constructor(
}
fun resolve(values: (String) -> String?): String? {
if (names.isEmpty())
return raw
val buffer = ArrayList<String>(pieces.size)
for (piece in pieces) {
@ -70,6 +73,9 @@ class SBPattern private constructor(
}
fun with(params: Map<String, String>): SBPattern {
if (names.isEmpty())
return this
val map = Object2ObjectArrayMap<String, String>()
map.putAll(this.params)
@ -89,14 +95,6 @@ class SBPattern private constructor(
fun resolve(map0: (String) -> String?, map1: (String) -> String?): String? {
return contents ?: map0.invoke(name!!) ?: map1.invoke(name!!)
}
fun resolve(map0: (String) -> String?): String? {
return contents ?: map0.invoke(name!!)
}
fun resolve(): String? {
return contents
}
}
companion object : TypeAdapterFactory {

View File

@ -0,0 +1,68 @@
package ru.dbotthepony.kstarbound.util
interface ITimeSource {
/**
* Время в наносекундах
*/
val nanos: Long
/**
* Время в микросекундах
*/
val micros: Long get() = nanos / 1_000L
/**
* Время в миллисекундах
*/
val millis: Long get() = nanos / 1_000_000L
/**
* Время в секундах, с точностью до микросекунд
*/
val seconds: Double get() = (nanos / 1_000L) / 1_000_000.0
}
class JVMTimeSource : ITimeSource {
private val origin = System.nanoTime()
override val nanos: Long
get() = System.nanoTime() - origin
companion object {
@JvmField
val INSTANCE = JVMTimeSource()
}
}
class ArtificialTimeSource(nanos: Long = 0L) : ITimeSource {
override var nanos: Long = nanos
private set
fun advance(nanos: Long) {
this.nanos += nanos
}
}
class PausableTimeSource(private val parent: ITimeSource) : ITimeSource {
override val nanos: Long get() {
if (isPaused) {
return pausedSince - skipped
} else {
return parent.nanos - skipped
}
}
private var isPaused = false
private var pausedSince = 0L
private var skipped = 0L
fun pause() {
if (!isPaused) {
isPaused = true
pausedSince = parent.nanos
} else {
isPaused = false
skipped += parent.nanos - pausedSince
}
}
}