Add playable corridor scroll desktop prototype
This commit is contained in:
43
HANDOFF_2026-06-19.md
Normal file
43
HANDOFF_2026-06-19.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Handoff — June 19, 2026
|
||||
|
||||
## 📌 Summary of Progress
|
||||
In this session, we successfully built and prototyped a fully playable MVP for the **Corridor Scroll** module using the desktop launcher.
|
||||
|
||||
1. **Environment & Build Fixes:**
|
||||
- Installed `openjdk@17` via Homebrew and set up Java environment variables.
|
||||
- Fixed desktop configuration in `desktop/build.gradle.kts` by adding the `:shared` dependency and passing `-XstartOnFirstThread` to resolve macOS GLFW thread restrictions.
|
||||
- Fixed `DesktopLauncher.kt` to instantiate the new `GameHost(ModuleType.LINE_CORRIDOR)` setup.
|
||||
2. **Gameplay Improvements (Corridor Scroll):**
|
||||
- Transformed the static horizontal prototype into a vertical wiggling corridor scrolling from bottom to top.
|
||||
- Slower scroll speed (`100f` px/s) to make drawing manageable.
|
||||
- Implemented an organic, unpredictable centerline using composite waves (a slow left-to-right drift combined with faster wiggles).
|
||||
- Designed a dynamic corridor width that swells up to 2x and pinches down to 0.5x of its base width as it scrolls.
|
||||
3. **Inputs & Feedback Loop:**
|
||||
- Added standard desktop mouse/touch drag fallback inside `LineCorridorScreen.kt` so the game is testable directly on macOS without an emulator.
|
||||
- Built a 15-second timer per session.
|
||||
- Programmed real-time scoring metrics:
|
||||
- **Containment:** % of drawn points staying within the boundaries.
|
||||
- **Fill:** Shading coverage between boundaries (calculated vertically in bins, normalized by the dynamic width of each bin).
|
||||
- **Overall:** Weighted combination.
|
||||
- Added end-game screens displaying scores and a restart prompt (SPACE/TAP to replay).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Current State
|
||||
* The desktop application compiles and runs successfully using:
|
||||
```bash
|
||||
export JAVA_HOME="$(brew --prefix openjdk@17)/libexec/openjdk.jdk/Contents/Home"
|
||||
./gradlew :desktop:run
|
||||
```
|
||||
* Gameplay is interactive, responsive, and wiggles/pinches behave as expected.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Next Steps & Feature Backlog
|
||||
1. **Pretentious Art Critique Commentary ("The Gallery Critique"):**
|
||||
- We drafted a feature proposal in `proposal_commentary.md`.
|
||||
- Next session can implement `ArtArchetype` in the `:shared` module and load corresponding art snob quotes (e.g., from *Dr. Alistair Vance* or *Madame Genevieve*) depending on whether the player was too safe (high containment, low fill) or too messy (low containment, high fill).
|
||||
2. **Android & Stylus Integration:**
|
||||
- Test the Android target and hook up actual S-Pen data (pressure/tilt) via `MotionEvent` APIs to make sure it handles S-Pen pressure.
|
||||
3. **Additional Modules:**
|
||||
- Begin development of Phase 2 modules like the **Pressure Ramp** (matching target pressure curves).
|
||||
@@ -3,16 +3,16 @@ package com.artlegend.modules
|
||||
import com.artlegend.input.StylusInputBridge
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.Screen
|
||||
import com.badlogic.gdx.Input
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.graphics.GL20
|
||||
import com.badlogic.gdx.graphics.OrthographicCamera
|
||||
import com.badlogic.gdx.graphics.g2d.BitmapFont
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
||||
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.random.Random
|
||||
import kotlin.math.cos
|
||||
|
||||
class LineCorridorScreen(
|
||||
private val bridge: StylusInputBridge,
|
||||
@@ -21,90 +21,158 @@ class LineCorridorScreen(
|
||||
|
||||
private lateinit var camera: OrthographicCamera
|
||||
private lateinit var shapeRenderer: ShapeRenderer
|
||||
private lateinit var spriteBatch: SpriteBatch
|
||||
private lateinit var font: BitmapFont
|
||||
|
||||
// Corridor geometry (LibGDX coords: origin bottom-left)
|
||||
private var corridorAngle = 0f // radians
|
||||
private var startX = 0f; private var startY = 0f
|
||||
private var endX = 0f; private var endY = 0f
|
||||
private val halfWidth = 55f // pixels — tight enough to be challenging
|
||||
// Level settings
|
||||
private val sessionDuration = 15f // 15 seconds session
|
||||
private val speed = 100f // Slightly faster (100 pixels per second)
|
||||
private val baseHalfWidth = 65f // base half-width of corridor
|
||||
|
||||
// Stroke
|
||||
private data class Pt(val x: Float, val y: Float, val pressure: Float)
|
||||
private val stroke = mutableListOf<Pt>()
|
||||
private var isDrawing = false
|
||||
// State
|
||||
private var time = 0f
|
||||
private var sessionDone = false
|
||||
|
||||
// Previous frame's isDown to detect edge transitions
|
||||
private var isDrawing = false
|
||||
private var prevDown = false
|
||||
|
||||
private data class Pt(val x: Float, val y: Float, val t: Float, val pressure: Float)
|
||||
private val strokes = mutableListOf<MutableList<Pt>>()
|
||||
private var currentStroke = mutableListOf<Pt>()
|
||||
|
||||
override fun show() {
|
||||
camera = OrthographicCamera()
|
||||
shapeRenderer = ShapeRenderer()
|
||||
regenerate()
|
||||
spriteBatch = SpriteBatch()
|
||||
font = BitmapFont()
|
||||
font.data.setScale(1.5f) // Make text highly readable
|
||||
resetSession()
|
||||
}
|
||||
|
||||
private fun regenerate() {
|
||||
private fun resetSession() {
|
||||
time = 0f
|
||||
sessionDone = false
|
||||
isDrawing = false
|
||||
prevDown = false
|
||||
strokes.clear()
|
||||
currentStroke = mutableListOf()
|
||||
|
||||
val w = Gdx.graphics.width.toFloat()
|
||||
val h = Gdx.graphics.height.toFloat()
|
||||
camera.setToOrtho(false, w, h)
|
||||
}
|
||||
|
||||
val deg = (Random.nextFloat() * 50f - 25f) // -25° to +25°
|
||||
corridorAngle = deg * (Math.PI / 180.0).toFloat()
|
||||
private fun getCorridorCenter(y: Float, t: Float): Float {
|
||||
val w = Gdx.graphics.width.toFloat()
|
||||
val centerX = w / 2f
|
||||
val u = y - speed * t
|
||||
|
||||
val cx = w / 2f
|
||||
val cy = h / 2f
|
||||
val halfLen = w * 0.40f
|
||||
val cos = cos(corridorAngle)
|
||||
val sin = sin(corridorAngle)
|
||||
// Composite waves: slow drift + faster wiggles
|
||||
val kDrift = (2 * Math.PI / 2400f).toFloat() // wavelength 2400
|
||||
val kWiggle = (2 * Math.PI / 600f).toFloat() // wavelength 600
|
||||
|
||||
startX = cx - halfLen * cos
|
||||
startY = cy - halfLen * sin
|
||||
endX = cx + halfLen * cos
|
||||
endY = cy + halfLen * sin
|
||||
// Drifts up to 220px from center, wiggles up to 90px around the drift path
|
||||
return centerX + 220f * sin(kDrift * u) + 90f * cos(kWiggle * u)
|
||||
}
|
||||
|
||||
private fun getHalfWidth(y: Float, t: Float): Float {
|
||||
val u = y - speed * t
|
||||
val widthWavelength = 1200f // width changes slower than the wiggles
|
||||
val kWidth = (2 * Math.PI / widthWavelength).toFloat()
|
||||
// factor oscillates between 0.5 (half as wide) and 2.0 (2x as wide)
|
||||
val factor = 1.25f + 0.75f * sin(kWidth * u)
|
||||
return baseHalfWidth * factor
|
||||
}
|
||||
|
||||
override fun render(delta: Float) {
|
||||
Gdx.gl.glClearColor(0.05f, 0.05f, 0.08f, 1f)
|
||||
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
|
||||
|
||||
if (!sessionDone) {
|
||||
time += delta
|
||||
processInput()
|
||||
|
||||
if (time >= sessionDuration) {
|
||||
sessionDone = true
|
||||
isDrawing = false
|
||||
if (currentStroke.size > 1) {
|
||||
strokes.add(currentStroke)
|
||||
}
|
||||
val finalScore = computeScore()
|
||||
onSessionComplete(finalScore)
|
||||
}
|
||||
} else {
|
||||
// Handle restart on spacebar press or click
|
||||
if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE) || (Gdx.input.justTouched() && !isDrawing)) {
|
||||
resetSession()
|
||||
}
|
||||
}
|
||||
|
||||
camera.update()
|
||||
shapeRenderer.projectionMatrix = camera.combined
|
||||
|
||||
// Draw corridor and path
|
||||
shapeRenderer.begin(ShapeRenderer.ShapeType.Line)
|
||||
drawCorridor()
|
||||
drawStroke()
|
||||
shapeRenderer.end()
|
||||
|
||||
// Draw UI Text Overlay
|
||||
spriteBatch.projectionMatrix = camera.combined
|
||||
spriteBatch.begin()
|
||||
if (!sessionDone) {
|
||||
val remaining = (sessionDuration - time).coerceAtLeast(0f)
|
||||
font.draw(spriteBatch, "Time Remaining: %.1fs".format(remaining), 20f, Gdx.graphics.height - 20f)
|
||||
font.draw(spriteBatch, "Wiggle back-and-forth to fill the space!", 20f, Gdx.graphics.height - 50f)
|
||||
} else {
|
||||
val score = computeScore()
|
||||
val cx = Gdx.graphics.width / 2f
|
||||
val cy = Gdx.graphics.height / 2f
|
||||
font.draw(spriteBatch, "SESSION COMPLETE!", cx - 120f, cy + 120f)
|
||||
font.draw(spriteBatch, "Containment Score: %.0f%%".format(score.straightness * 100f), cx - 150f, cy + 60f)
|
||||
font.draw(spriteBatch, "Fill Score: %.0f%%".format(score.anglePrecision * 100f), cx - 150f, cy + 20f)
|
||||
font.draw(spriteBatch, "Overall Score: %.0f%%".format(score.overall * 100f), cx - 150f, cy - 20f)
|
||||
font.draw(spriteBatch, "Press SPACE or TAP to play again", cx - 180f, cy - 100f)
|
||||
}
|
||||
spriteBatch.end()
|
||||
}
|
||||
|
||||
private fun processInput() {
|
||||
val frame = bridge.latestFrame ?: return
|
||||
if (sessionDone) return
|
||||
|
||||
// Flip Android Y (top-left origin) to LibGDX Y (bottom-left origin)
|
||||
val gdxY = Gdx.graphics.height - frame.y
|
||||
val down = frame.isDown
|
||||
val frame = bridge.latestFrame
|
||||
val down: Boolean
|
||||
val screenX: Float
|
||||
val screenY: Float
|
||||
val pressure: Float
|
||||
|
||||
// DOWN edge: start capturing
|
||||
if (frame != null) {
|
||||
down = frame.isDown
|
||||
screenX = frame.x
|
||||
screenY = Gdx.graphics.height - frame.y
|
||||
pressure = frame.pressure
|
||||
} else {
|
||||
// Desktop fallback: route mouse coordinates to drawing
|
||||
down = Gdx.input.isTouched
|
||||
screenX = Gdx.input.x.toFloat()
|
||||
screenY = (Gdx.graphics.height - Gdx.input.y).toFloat()
|
||||
pressure = 1f
|
||||
}
|
||||
|
||||
// DOWN edge
|
||||
if (down && !prevDown) {
|
||||
isDrawing = true
|
||||
stroke.clear()
|
||||
currentStroke = mutableListOf()
|
||||
}
|
||||
|
||||
if (isDrawing && down) {
|
||||
stroke.add(Pt(frame.x, gdxY, frame.pressure))
|
||||
currentStroke.add(Pt(screenX, screenY, time, pressure))
|
||||
}
|
||||
|
||||
// UP edge: score and finish
|
||||
// UP edge
|
||||
if (!down && prevDown && isDrawing) {
|
||||
isDrawing = false
|
||||
if (stroke.size > 1) {
|
||||
sessionDone = true
|
||||
val score = computeScore()
|
||||
Gdx.app.log("LineCorridor", "straightness=%.2f angle=%.2f overall=%.2f"
|
||||
.format(score.straightness, score.anglePrecision, score.overall))
|
||||
onSessionComplete(score)
|
||||
if (currentStroke.size > 1) {
|
||||
strokes.add(currentStroke)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,85 +180,153 @@ class LineCorridorScreen(
|
||||
}
|
||||
|
||||
private fun drawCorridor() {
|
||||
val perpX = -sin(corridorAngle)
|
||||
val perpY = cos(corridorAngle)
|
||||
val h = Gdx.graphics.height.toFloat()
|
||||
|
||||
val ox = perpX * halfWidth
|
||||
val oy = perpY * halfWidth
|
||||
|
||||
// Corridor walls
|
||||
// Draw scrolling walls
|
||||
shapeRenderer.color = Color(0f, 0.85f, 1f, 0.75f)
|
||||
shapeRenderer.line(startX + ox, startY + oy, endX + ox, endY + oy)
|
||||
shapeRenderer.line(startX - ox, startY - oy, endX - ox, endY - oy)
|
||||
val step = 10f
|
||||
var prevLeftX = getCorridorCenter(0f, time) - getHalfWidth(0f, time)
|
||||
var prevRightX = getCorridorCenter(0f, time) + getHalfWidth(0f, time)
|
||||
var prevY = 0f
|
||||
|
||||
// End caps
|
||||
shapeRenderer.line(startX + ox, startY + oy, startX - ox, startY - oy)
|
||||
shapeRenderer.line(endX + ox, endY + oy, endX - ox, endY - oy)
|
||||
var y = step
|
||||
while (y <= h) {
|
||||
val centerX = getCorridorCenter(y, time)
|
||||
val hw = getHalfWidth(y, time)
|
||||
val leftX = centerX - hw
|
||||
val rightX = centerX + hw
|
||||
|
||||
// Dashed centerline
|
||||
shapeRenderer.line(prevLeftX, prevY, leftX, y)
|
||||
shapeRenderer.line(prevRightX, prevY, rightX, y)
|
||||
|
||||
prevLeftX = leftX
|
||||
prevRightX = rightX
|
||||
prevY = y
|
||||
y += step
|
||||
}
|
||||
|
||||
// Draw dotted centerline
|
||||
shapeRenderer.color = Color(0f, 0.85f, 1f, 0.25f)
|
||||
val dx = endX - startX
|
||||
val dy = endY - startY
|
||||
val len = sqrt(dx * dx + dy * dy)
|
||||
val dashCount = (len / 24f).toInt()
|
||||
for (i in 0 until dashCount) {
|
||||
if (i % 2 == 1) continue // every other segment is a gap
|
||||
val t0 = i / dashCount.toFloat()
|
||||
val t1 = (i + 1) / dashCount.toFloat()
|
||||
shapeRenderer.line(
|
||||
startX + dx * t0, startY + dy * t0,
|
||||
startX + dx * t1, startY + dy * t1
|
||||
)
|
||||
prevLeftX = getCorridorCenter(0f, time)
|
||||
prevY = 0f
|
||||
y = step
|
||||
var dashIndex = 0
|
||||
while (y <= h) {
|
||||
val centerX = getCorridorCenter(y, time)
|
||||
if (dashIndex % 2 == 0) {
|
||||
shapeRenderer.line(prevLeftX, prevY, centerX, y)
|
||||
}
|
||||
prevLeftX = centerX
|
||||
prevY = y
|
||||
y += step
|
||||
dashIndex++
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawStroke() {
|
||||
if (stroke.size < 2) return
|
||||
// Render past completed strokes
|
||||
for (stroke in strokes) {
|
||||
if (stroke.size < 2) continue
|
||||
for (i in 0 until stroke.size - 1) {
|
||||
val a = stroke[i]
|
||||
val b = stroke[i + 1]
|
||||
|
||||
val ay = a.y + speed * (time - a.t)
|
||||
val by = b.y + speed * (time - b.t)
|
||||
|
||||
if (ay < Gdx.graphics.height || by < Gdx.graphics.height) {
|
||||
val alpha = a.pressure.coerceIn(0.15f, 1f)
|
||||
shapeRenderer.color = Color(1f, 0.85f, 0.25f, alpha)
|
||||
shapeRenderer.line(a.x, a.y, b.x, b.y)
|
||||
shapeRenderer.line(a.x, ay, b.x, by)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render current active stroke
|
||||
if (isDrawing && currentStroke.size >= 2) {
|
||||
for (i in 0 until currentStroke.size - 1) {
|
||||
val a = currentStroke[i]
|
||||
val b = currentStroke[i + 1]
|
||||
|
||||
val ay = a.y + speed * (time - a.t)
|
||||
val by = b.y + speed * (time - b.t)
|
||||
|
||||
if (ay < Gdx.graphics.height || by < Gdx.graphics.height) {
|
||||
val alpha = a.pressure.coerceIn(0.15f, 1f)
|
||||
shapeRenderer.color = Color(1f, 0.85f, 0.25f, alpha)
|
||||
shapeRenderer.line(a.x, ay, b.x, by)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeScore(): LineScore {
|
||||
val dx = endX - startX
|
||||
val dy = endY - startY
|
||||
val lenSq = dx * dx + dy * dy
|
||||
|
||||
var sumSqDev = 0.0
|
||||
for (pt in stroke) {
|
||||
val t = ((pt.x - startX) * dx + (pt.y - startY) * dy) / lenSq
|
||||
val cx = startX + t * dx
|
||||
val cy = startY + t * dy
|
||||
val distX = pt.x - cx
|
||||
val distY = pt.y - cy
|
||||
sumSqDev += distX * distX + distY * distY
|
||||
val allPoints = mutableListOf<Pt>()
|
||||
for (s in strokes) {
|
||||
allPoints.addAll(s)
|
||||
}
|
||||
if (isDrawing) {
|
||||
allPoints.addAll(currentStroke)
|
||||
}
|
||||
|
||||
val rms = sqrt(sumSqDev / stroke.size).toFloat()
|
||||
val straightness = (1f - (rms / halfWidth)).coerceIn(0f, 1f)
|
||||
if (allPoints.isEmpty()) {
|
||||
return LineScore(0f, 0f, 0f)
|
||||
}
|
||||
|
||||
// Angle of drawn stroke (first → last point), scored against corridorAngle.
|
||||
// ±15° tolerance: 0° diff → 1.0, 15° diff → 0.0.
|
||||
val first = stroke.first()
|
||||
val last = stroke.last()
|
||||
val strokeAngle = atan2((last.y - first.y).toDouble(), (last.x - first.x).toDouble()).toFloat()
|
||||
var angleDiff = abs(strokeAngle - corridorAngle)
|
||||
// Clamp to [0, π/2] to handle π-equivalent opposite directions
|
||||
if (angleDiff > Math.PI.toFloat() / 2f) angleDiff = Math.PI.toFloat() - angleDiff
|
||||
val maxAngleDiff = (15f * (Math.PI / 180.0)).toFloat()
|
||||
val anglePrecision = (1f - (angleDiff / maxAngleDiff)).coerceIn(0f, 1f)
|
||||
// 1. Containment Score (percentage of points drawn inside the corridor)
|
||||
var insideCount = 0
|
||||
for (pt in allPoints) {
|
||||
val xc = getCorridorCenter(pt.y, pt.t)
|
||||
val hw = getHalfWidth(pt.y, pt.t)
|
||||
val dist = abs(pt.x - xc)
|
||||
if (dist <= hw) {
|
||||
insideCount++
|
||||
}
|
||||
}
|
||||
val containment = insideCount.toFloat() / allPoints.size
|
||||
|
||||
val overall = straightness * 0.6f + anglePrecision * 0.4f
|
||||
// 2. Fill Score (evaluate local horizontal coverage inside bins)
|
||||
val binSize = 30f
|
||||
val uValues = allPoints.map { it.y - speed * it.t }
|
||||
val minU = uValues.minOrNull() ?: 0f
|
||||
val maxU = uValues.maxOrNull() ?: 0f
|
||||
val spanU = maxU - minU
|
||||
|
||||
return LineScore(straightness, anglePrecision, overall)
|
||||
val binMap = mutableMapOf<Int, MutableList<Float>>()
|
||||
for (pt in allPoints) {
|
||||
val u = pt.y - speed * pt.t
|
||||
val binIndex = (u / binSize).toInt()
|
||||
val xc = getCorridorCenter(pt.y, pt.t)
|
||||
val localX = pt.x - xc
|
||||
binMap.getOrPut(binIndex) { mutableListOf() }.add(localX)
|
||||
}
|
||||
|
||||
var totalBinScore = 0f
|
||||
var scoredBins = 0
|
||||
for ((binIndex, wList) in binMap) {
|
||||
val u = binIndex * binSize
|
||||
val hw = getHalfWidth(u, 0f)
|
||||
val maxW = wList.maxOrNull() ?: 0f
|
||||
val minW = wList.minOrNull() ?: 0f
|
||||
val spread = (maxW - minW)
|
||||
val binFill = (spread / (2 * hw)).coerceAtMost(1f)
|
||||
totalBinScore += binFill
|
||||
scoredBins++
|
||||
}
|
||||
|
||||
var fill = if (scoredBins > 0) totalBinScore / scoredBins else 0f
|
||||
|
||||
// Scale down if player drew over a very short section of the corridor
|
||||
val lengthScale = (spanU / 300f).coerceIn(0f, 1f)
|
||||
fill *= lengthScale
|
||||
|
||||
val overall = containment * 0.5f + fill * 0.5f
|
||||
|
||||
return LineScore(containment, fill, overall)
|
||||
}
|
||||
|
||||
override fun resize(width: Int, height: Int) {
|
||||
if (!sessionDone) regenerate()
|
||||
camera.setToOrtho(false, width.toFloat(), height.toFloat())
|
||||
}
|
||||
|
||||
override fun pause() {}
|
||||
@@ -199,5 +335,7 @@ class LineCorridorScreen(
|
||||
|
||||
override fun dispose() {
|
||||
shapeRenderer.dispose()
|
||||
spriteBatch.dispose()
|
||||
font.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ val gdxVersion: String by project
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
implementation(project(":shared"))
|
||||
implementation("com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion")
|
||||
implementation("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop")
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("com.artlegend.DesktopLauncherKt")
|
||||
applicationDefaultJvmArgs = listOf("-XstartOnFirstThread")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.artlegend
|
||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
|
||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
|
||||
|
||||
import com.artlegend.shared.ModuleType
|
||||
|
||||
fun main() {
|
||||
val config = Lwjgl3ApplicationConfiguration().apply {
|
||||
setTitle("ArtLegend")
|
||||
@@ -10,5 +12,5 @@ fun main() {
|
||||
setForegroundFPS(60)
|
||||
useVsync(true)
|
||||
}
|
||||
Lwjgl3Application(ArtLegend(), config)
|
||||
Lwjgl3Application(GameHost(ModuleType.LINE_CORRIDOR), config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user