Controller: Separate game and view loops

This commit is contained in:
LoveEevee
2019-01-16 15:33:42 +03:00
parent 926b163460
commit 3398791afe
20 changed files with 275 additions and 275 deletions

View File

@@ -6,7 +6,7 @@
]
this.touchEnabled = touchEnabled
loader.changePage("about")
loader.changePage("about", true)
cancelTouch = false
this.endButton = document.getElementById("tutorial-end-button")

View File

@@ -1,4 +1,42 @@
var assets = {
"js": [
"lib/fontdetect.min.js",
"loadsong.js",
"parseosu.js",
"titlescreen.js",
"scoresheet.js",
"songselect.js",
"keyboard.js",
"game.js",
"controller.js",
"circle.js",
"view.js",
"mekadon.js",
"gamepad.js",
"tutorial.js",
"soundbuffer.js",
"p2.js",
"canvasasset.js",
"viewassets.js",
"gamerules.js",
"canvasdraw.js",
"canvastest.js",
"canvascache.js",
"parsetja.js",
"about.js",
"debug.js",
"session.js",
"strings.js",
"importsongs.js"
],
"css": [
"main.css",
"titlescreen.css",
"loadsong.css",
"game.css",
"debug.css",
"songbg.css"
],
"img": [
"title-screen.png",
"logo-big.png",

View File

@@ -1,7 +1,7 @@
class CanvasAsset{
constructor(view, layer, position){
this.ctx = view.ctx
this.controller = view.controller
this.view = view
this.position = position
this.animationFrames = {}
this.speed = 1000 / 60
@@ -13,7 +13,7 @@ class CanvasAsset{
if(this.animation){
var u = (a, b) => typeof a === "undefined" ? b : a
var frame = 0
var ms = this.controller.getElapsedTime()
var ms = this.view.getMS()
var beatInterval = this.frameSpeed ? 1000 / 60 : this.beatInterval
if(this.animationEnd){
@@ -95,7 +95,7 @@ class CanvasAsset{
}
changeBeatInterval(beatMS, initial){
if(!initial && !this.frameSpeed){
var ms = this.controller.getElapsedTime()
var ms = this.view.getMS()
this.animationStart = ms - (ms - this.animationStart) / this.beatInterval * beatMS
}
this.beatInterval = beatMS

View File

@@ -7,6 +7,10 @@ class Controller{
this.touchEnabled = touchEnabled
this.snd = this.multiplayer ? "_p" + this.multiplayer : ""
if(this.multiplayer !== 2){
loader.changePage("game", false)
}
if(selectedSong.type === "tja"){
this.parsedSongData = new ParseTja(songData, selectedSong.difficulty, selectedSong.offset)
}else{
@@ -47,27 +51,48 @@ class Controller{
})
}
startMainLoop(){
this.mainLoopStarted = false
this.mainLoopRunning = true
this.mainLoop()
this.gameLoop()
this.viewLoop()
this.gameInterval = setInterval(this.gameLoop.bind(this), 1000 / 60)
}
stopMainLoop(){
this.mainLoopRunning = false
this.mainAsset.stop()
clearInterval(this.gameInterval)
}
mainLoop(){
gameLoop(){
if(this.mainLoopRunning){
if(this.syncWith){
this.syncWith.game.elapsedTime = this.game.elapsedTime
this.syncWith.game.startDate = this.game.startDate
}
var ms = this.game.elapsedTime
this.keyboard.checkMenuKeys()
if(!this.game.isPaused()){
this.keyboard.checkGameKeys()
if(ms < 0){
this.game.updateTime()
}else{
this.game.update()
if(!this.mainLoopRunning){
return
}
this.game.playMainMusic()
}
}
}
}
viewLoop(){
if(this.mainLoopRunning){
if(this.multiplayer !== 2){
requestAnimationFrame(() => {
this.viewLoop()
if(this.syncWith){
this.syncWith.game.elapsedTime = this.game.elapsedTime
this.syncWith.game.startDate = this.game.startDate
this.syncWith.viewLoop()
}
this.mainLoop()
if(this.syncWith){
this.syncWith.mainLoop()
}
if(this.scoresheet){
if(this.view.ctx){
this.view.ctx.save()
@@ -80,27 +105,7 @@ class Controller{
}
})
}
var ms = this.game.elapsedTime
if(!this.game.isPaused()){
this.keyboard.checkGameKeys()
if(ms >= 0 && !this.mainLoopStarted){
this.mainLoopStarted = true
}
if(ms < 0){
this.game.updateTime()
}
if(this.mainLoopStarted){
this.game.update()
if(!this.mainLoopRunning){
return
}
this.game.playMainMusic()
}
}
this.view.refresh()
this.keyboard.checkMenuKeys()
}
}
gameEnded(){
@@ -130,7 +135,6 @@ class Controller{
if(!fadeIn){
this.clean()
}
loader.screen.classList.remove("view")
new SongSelect(false, fadeIn, this.touchEnabled)
}
restartSong(){
@@ -138,7 +142,6 @@ class Controller{
if(this.multiplayer){
new LoadSong(this.selectedSong, false, true, this.touchEnabled)
}else{
loader.changePage("game")
var taikoGame = new Controller(this.selectedSong, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
taikoGame.run()
}

View File

@@ -64,7 +64,8 @@ class Game{
updateCirclesStatus(){
var nextSet = false
var circles = this.songData.circles
for(var i in circles){
var startIndex = this.currentCircle === 0 ? 0 : this.currentCircle - 1
for(var i = startIndex; i < circles.length && i < this.currentCircle + 2; i++){
var circle = circles[i]
if(!circle.getPlayed()){
var ms = this.elapsedTime

View File

@@ -28,21 +28,23 @@ class Gamepad{
if(callback){
this.interval = setInterval(() => {
this.play(callback)
}, 100)
}, 1000 / 60)
}
}
play(callback){
if(pageEvents.lastKeyEvent + 5000 > Date.now()){
return
}
if("getGamepads" in navigator){
var gamepads = navigator.getGamepads()
if(gamepads.length === 0){
return
}
}else{
return
}
if(pageEvents.lastKeyEvent + 5000 > Date.now()){
return
}
var bindings = this.bindings
var force = {
lsu: false,
lsr: false,

View File

@@ -35,6 +35,7 @@ class Keyboard{
gameBtn[this.kbd["ka_l"]] = ["lb", "lt"]
gameBtn[this.kbd["ka_r"]] = ["rb", "rt"]
this.gamepad = new Gamepad(gameBtn)
this.gamepadInterval = setInterval(this.gamepadKeys.bind(this), 1000 / 60 / 2)
var menuBtn = {
"cancel": ["a"],
@@ -84,23 +85,25 @@ class Keyboard{
return true
}
checkGameKeys(){
if(!this.controller.autoPlayEnabled){
var ms = this.game.getAccurateTime()
if(this.controller.autoPlayEnabled){
this.checkKeySound(this.kbd["don_l"], "don")
this.checkKeySound(this.kbd["don_r"], "don")
this.checkKeySound(this.kbd["ka_l"], "ka")
this.checkKeySound(this.kbd["ka_r"], "ka")
}
}
gamepadKeys(){
if(!this.game.isPaused() && !this.controller.autoPlayEnabled){
this.gamepad.play((pressed, keyCode) => {
if(pressed){
if(this.keys[keyCode]){
this.setKey(keyCode, false)
}
this.setKey(keyCode, true, ms)
this.setKey(keyCode, true, this.game.getAccurateTime())
}else{
this.setKey(keyCode, false)
}
})
}else{
this.checkKeySound(this.kbd["don_l"], "don")
this.checkKeySound(this.kbd["don_r"], "don")
this.checkKeySound(this.kbd["ka_l"], "ka")
this.checkKeySound(this.kbd["ka_r"], "ka")
}
}
checkMenuKeys(){
@@ -239,5 +242,6 @@ class Keyboard{
}
clean(){
pageEvents.keyRemove(this, "all")
clearInterval(this.gamepadInterval)
}
}

View File

@@ -3,92 +3,141 @@ class Loader{
this.callback = callback
this.loadedAssets = 0
this.assetsDiv = document.getElementById("assets")
this.canvasTest = new CanvasTest()
this.screen = document.getElementById("screen")
this.startTime = Date.now()
this.ajax("/src/views/loader.html").then(this.run.bind(this))
var promises = []
promises.push(this.ajax("/src/views/loader.html").then(page => {
this.screen.innerHTML = page
}))
promises.push(this.ajax("/api/config").then(conf => {
gameConfig = JSON.parse(conf)
}))
Promise.all(promises).then(this.run.bind(this))
}
run(page){
run(){
this.promises = []
this.screen = document.getElementById("screen")
this.screen.innerHTML = page
this.loaderPercentage = document.querySelector("#loader .percentage")
this.loaderProgress = document.querySelector("#loader .progress")
snd.buffer = new SoundBuffer()
snd.musicGain = snd.buffer.createGain()
snd.sfxGain = snd.buffer.createGain()
snd.previewGain = snd.buffer.createGain()
snd.sfxGainL = snd.buffer.createGain("left")
snd.sfxGainR = snd.buffer.createGain("right")
snd.sfxLoudGain = snd.buffer.createGain()
snd.buffer.setCrossfade(
[snd.musicGain, snd.previewGain],
[snd.sfxGain, snd.sfxGainL, snd.sfxGainR],
0.5
)
snd.sfxLoudGain.setVolume(1.2)
this.promises.push(this.ajax("/api/config").then(conf => {
gameConfig = JSON.parse(conf)
snd.buffer.load(gameConfig.assets_baseurl + "audio/" + assets.audioOgg).then(() => {
var queryString = gameConfig._version ? "?" + gameConfig._version.commit_short : ""
assets.js.forEach(name => {
var script = document.createElement("script")
this.addPromise(pageEvents.load(script))
script.src = "/src/js/" + name + queryString
document.head.appendChild(script)
})
this.addPromise(new Promise(resolve => {
var cssCount = document.styleSheets.length + assets.css.length
assets.css.forEach(name => {
var stylesheet = document.createElement("link")
stylesheet.rel = "stylesheet"
stylesheet.href = "/src/css/" + name + queryString
document.head.appendChild(stylesheet)
})
var checkStyles = () => {
if(document.styleSheets.length >= cssCount){
resolve()
clearInterval(interval)
}
}
var interval = setInterval(checkStyles, 100)
checkStyles()
}))
assets.fonts.forEach(name => {
var font = document.createElement("h1")
font.style.fontFamily = name
font.appendChild(document.createTextNode("I am a font"))
this.assetsDiv.appendChild(font)
})
assets.img.forEach(name => {
var id = this.getFilename(name)
var image = document.createElement("img")
this.addPromise(pageEvents.load(image))
image.id = name
image.src = gameConfig.assets_baseurl + "img/" + name
this.assetsDiv.appendChild(image)
assets.image[id] = image
})
assets.views.forEach(name => {
var id = this.getFilename(name)
this.addPromise(this.ajax("/src/views/" + name + queryString).then(page => {
assets.pages[id] = page
}))
})
this.addPromise(this.ajax("/api/songs").then(songs => {
assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault
}))
this.afterJSCount =
[assets.audioOgg, "blurPerformance", "P2Connection"].length +
assets.fonts.length +
assets.audioSfx.length +
assets.audioMusic.length +
assets.audioSfxLR.length +
assets.audioSfxLoud.length
Promise.all(this.promises).then(() => {
snd.buffer = new SoundBuffer()
snd.musicGain = snd.buffer.createGain()
snd.sfxGain = snd.buffer.createGain()
snd.previewGain = snd.buffer.createGain()
snd.sfxGainL = snd.buffer.createGain("left")
snd.sfxGainR = snd.buffer.createGain("right")
snd.sfxLoudGain = snd.buffer.createGain()
snd.buffer.setCrossfade(
[snd.musicGain, snd.previewGain],
[snd.sfxGain, snd.sfxGainL, snd.sfxGainR],
0.5
)
snd.sfxLoudGain.setVolume(1.2)
this.afterJSCount--
this.addPromise(snd.buffer.load(gameConfig.assets_baseurl + "audio/" + assets.audioOgg).then(() => {
this.oggNotSupported = false
}, () => {
this.oggNotSupported = true
}).then(() => {
this.afterJSCount = 0
assets.fonts.forEach(name => {
var font = document.createElement("h1")
font.style.fontFamily = name
font.appendChild(document.createTextNode("I am a font"))
this.assetsDiv.appendChild(font)
this.promises.push(new Promise((resolve, reject) => {
FontDetect.onFontLoaded(name, resolve, reject, {msTimeout: 90000})
this.addPromise(new Promise(resolve => {
FontDetect.onFontLoaded(name, resolve, resolve, {msTimeout: Infinity})
}))
})
assets.img.forEach(name => {
var id = this.getFilename(name)
var image = document.createElement("img")
this.promises.push(pageEvents.load(image))
image.id = name
image.src = gameConfig.assets_baseurl + "img/" + name
this.assetsDiv.appendChild(image)
assets.image[id] = image
})
assets.audioSfx.forEach(name => {
this.promises.push(this.loadSound(name, snd.sfxGain))
this.addPromise(this.loadSound(name, snd.sfxGain))
})
assets.audioMusic.forEach(name => {
this.promises.push(this.loadSound(name, snd.musicGain))
this.addPromise(this.loadSound(name, snd.musicGain))
})
assets.audioSfxLR.forEach(name => {
this.promises.push(this.loadSound(name, snd.sfxGain).then(sound => {
this.addPromise(this.loadSound(name, snd.sfxGain).then(sound => {
var id = this.getFilename(name)
assets.sounds[id + "_p1"] = assets.sounds[id].copy(snd.sfxGainL)
assets.sounds[id + "_p2"] = assets.sounds[id].copy(snd.sfxGainR)
}))
})
assets.audioSfxLoud.forEach(name => {
this.promises.push(this.loadSound(name, snd.sfxLoudGain))
this.addPromise(this.loadSound(name, snd.sfxLoudGain))
})
this.promises.push(this.ajax("/api/songs").then(songs => {
assets.songsDefault = JSON.parse(songs)
assets.songs = assets.songsDefault
}))
assets.views.forEach(name => {
var id = this.getFilename(name)
var qs = gameConfig._version ? '?' + gameConfig._version.commit_short : '?'
this.promises.push(this.ajax("/src/views/" + name + qs).then(page => {
assets.pages[id] = page
}))
})
this.promises.push(this.canvasTest.blurPerformance().then(result => {
this.canvasTest = new CanvasTest()
this.addPromise(this.canvasTest.blurPerformance().then(result => {
perf.blur = result
if(result > 1000 / 50){
// Less than 50 fps with blur enabled
@@ -96,8 +145,10 @@ class Loader{
}
}))
p2 = new P2Connection()
if(location.hash.length === 6){
this.promises.push(new Promise(resolve => {
p2.hashLock = true
this.addPromise(new Promise(resolve => {
p2.open()
pageEvents.add(p2, "message", response => {
if(response.type === "session"){
@@ -119,12 +170,10 @@ class Loader{
}).then(() => {
pageEvents.remove(p2, "message")
}))
}else{
p2.hash("")
}
this.promises.forEach(promise => {
promise.then(this.assetLoaded.bind(this))
})
Promise.all(this.promises).then(() => {
this.canvasTest.drawAllImages().then(result => {
perf.allImg = result
@@ -135,9 +184,13 @@ class Loader{
})
}, this.errorMsg.bind(this))
})
}))
}))
})
}
addPromise(promise){
this.promises.push(promise)
promise.then(this.assetLoaded.bind(this))
}
loadSound(name, gain){
if(this.oggNotSupported && name.endsWith(".ogg")){
@@ -161,13 +214,14 @@ class Loader{
assetLoaded(){
if(!this.error){
this.loadedAssets++
var percentage = Math.floor(this.loadedAssets * 100 / this.promises.length)
var percentage = Math.floor(this.loadedAssets * 100 / (this.promises.length + this.afterJSCount))
this.loaderProgress.style.width = percentage + "%"
this.loaderPercentage.firstChild.data = percentage + "%"
}
}
changePage(name){
changePage(name, patternBg){
this.screen.innerHTML = assets.pages[name]
this.screen.classList[patternBg ? "add" : "remove"]("pattern-bg")
}
ajax(url, customRequest){
return new Promise((resolve, reject) => {

View File

@@ -5,7 +5,7 @@ class LoadSong{
this.multiplayer = multiplayer
this.touchEnabled = touchEnabled
loader.changePage("loadsong")
loader.changePage("loadsong", true)
var loadingText = document.getElementById("loading-text")
loadingText.appendChild(document.createTextNode(strings.loading))
loadingText.setAttribute("alt", strings.loading)
@@ -233,7 +233,6 @@ class LoadSong{
}else if(event.type === "gamestart"){
this.clean()
p2.clearMessage("songsel")
loader.changePage("game")
var taikoGame1 = new Controller(song, this.songData, false, 1, this.touchEnabled)
var taikoGame2 = new Controller(this.selectedSong2, this.song2Data, true, 2, this.touchEnabled)
taikoGame1.run(taikoGame2)
@@ -248,7 +247,6 @@ class LoadSong{
})
}else{
this.clean()
loader.changePage("game")
var taikoGame = new Controller(song, this.songData, this.autoPlayEnabled, false, this.touchEnabled)
taikoGame.run()
}

View File

@@ -58,11 +58,16 @@ function debug(){
}
var root = document.documentElement
var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscreen" in root || "mozRequestFullScreen" in root
if(/iPhone|iPad/.test(navigator.userAgent)){
var fullScreenSupported = false
}else{
var fullScreenSupported = "requestFullscreen" in root || "webkitRequestFullscreen" in root || "mozRequestFullScreen" in root
}
var pageEvents = new PageEvents()
var snd = {}
var p2 = new P2Connection()
var p2
var disableBlur = false
var cancelTouch = true
var lastHeight
@@ -104,11 +109,6 @@ pageEvents.keyAdd(debugObj, "all", "down", event => {
debugObj.controller.restartSong()
}
})
if(location.hash.length === 6){
p2.hashLock = true
}else{
p2.hash("")
}
var loader = new Loader(() => {
new Titlescreen()

View File

@@ -290,7 +290,7 @@ class ParseOsu{
var extras = values.slice(this.osu.EXTRAS)
var distance = parseFloat(extras[this.osu.PIXELLENGTH])
var distance = parseFloat(extras[this.osu.PIXELLENGTH]) * parseFloat(extras[this.osu.REPEAT])
var velocity = this.difficulty.sliderMultiplier * speed / 10
var endTime = start + distance / velocity

View File

@@ -493,7 +493,7 @@ class Scoresheet{
fontSize: 29,
fontFamily: this.font,
align: "right",
width: 215,
width: 154,
letterSpacing: 1
}, [
{outline: "#000", letterBorder: 8},

View File

@@ -1,7 +1,7 @@
class Session{
constructor(touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("session")
loader.changePage("session", true)
this.endButton = document.getElementById("tutorial-end-button")
if(touchEnabled){
document.getElementById("tutorial-outer").classList.add("touch-enabled")

View File

@@ -1,11 +1,11 @@
class SongSelect{
constructor(fromTutorial, fadeIn, touchEnabled){
this.touchEnabled = touchEnabled
loader.changePage("songselect")
loader.changePage("songselect", false)
this.canvas = document.getElementById("song-sel-canvas")
this.ctx = this.canvas.getContext("2d")
this.songSkin = {
"selected": {
background: "#ffdb2c",

View File

@@ -1,6 +1,6 @@
class Titlescreen{
constructor(){
loader.changePage("titlescreen")
loader.changePage("titlescreen", false)
this.titleScreen = document.getElementById("title-screen")
var proceed = document.getElementById("title-proceed")
proceed.appendChild(document.createTextNode(strings.titleProceed))

View File

@@ -1,7 +1,7 @@
class Tutorial{
constructor(fromSongSel){
this.fromSongSel = fromSongSel
loader.changePage("tutorial")
loader.changePage("tutorial", true)
assets.sounds["bgm_setsume"].playLoop(0.1, false, 0, 1.054, 16.054)
this.endButton = document.getElementById("tutorial-end-button")

View File

@@ -124,7 +124,7 @@
}
this.setDonBg()
this.lastMousemove = this.controller.getElapsedTime()
this.lastMousemove = this.controller.game.getAccurateTime()
pageEvents.mouseAdd(this, this.onmousemove.bind(this))
this.refresh()
@@ -180,7 +180,10 @@
}
winW /= ratio
winH /= ratio
var ms = this.getMS()
if(!this.controller.game.paused){
this.ms = this.controller.game.getAccurateTime()
}
var ms = this.ms
if(this.portrait){
var frameTop = winH / 2 - 1280 / 2
@@ -978,7 +981,6 @@
}else{
var catId = this.categories.default.sort
}
loader.screen.classList.add("view")
if(!selectedSong.songSkin.song){
var id = selectedSong.songBg
@@ -1100,7 +1102,7 @@
}
drawCircles(circles){
var distanceForCircle = this.winW / this.ratio - this.slotPos.x
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
for(var i = circles.length; i--;){
var circle = circles[i]
@@ -1127,7 +1129,7 @@
}
}
drawAnimatedCircles(circles){
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
for(var i = 0; i < circles.length; i++){
var circle = circles[i]
@@ -1174,7 +1176,7 @@
var fill, size, faceID
var type = circle.getType()
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
var circleMs = circle.getMS()
var endTime = circle.getEndTime()
var animated = circle.isAnimated()
@@ -1458,7 +1460,7 @@
&& animation !== "gogo"
){
don.setAnimation("10combo")
var ms = this.controller.getElapsedTime()
var ms = this.getMS()
don.setAnimationStart(ms)
var length = don.getAnimationLength("normal")
don.setUpdateSpeed(4 / length)
@@ -1677,7 +1679,7 @@
this.assets.changeBeatInterval(beatMS)
}
getMS(){
return this.controller.getElapsedTime()
return this.ms
}
clean(){
this.draw.clean()