DanisRace/Scripts/Opt.py

527 lines
17 KiB
Python
Raw Permalink Normal View History

2024-07-13 15:15:50 +02:00
import bge
import math
import numpy
import mathutils
r = math.radians
from Scripts import Reuse
from Scripts.Common import *
# Chunks to store data about parts of the world
chunks = {}
bge.logic.globalDict["Opt.chunks"] = chunks
camspeed = 0.0
camposition = mathutils.Vector((0,0,0))
camspeedframe = 0.0
def CameraSpeed():
# This function gives the average speed of the
# camera at any given frame.
frame = round(bge.logic.getRealTime(), 2)
global camspeed
global camposition
global camspeedframe
if frame != camspeedframe:
camspeedframe = frame
# Getting velocity of the camera
p = bge.logic.getCurrentScene().active_camera.worldPosition
v = p - camposition
camposition = p.copy()
# Averaging the velocity
av = 0
for i in v:
if i > 0: av += i
else : av -= i
av /= 3
# Storing the value
camspeed = av
# Returning the value
return camspeed
def ToFPS():
# Return a fraction that is useful to adjust FPS
# sensitive values like gravity.
return 60/bge.logic.getAverageFrameRate()
2024-08-27 14:34:26 +02:00
2024-07-13 15:15:50 +02:00
# Global Variables for GoodFPS function to work.
fpshistory = {} # History of last 200 or so frames FPS
targetFPS = 1 # Target FPS which to aim to preserve
FPStargets = {} # Database of how much a given function effects FPS
testingTarget = False # Fuction that is being tested for how much it effects FPS
testingTargetFrame = 0 # Frame where the test is started
testFPS = 60 # The FPS before the test started
limitframe = 0 # The Frame that is currently being calculated for expected FPS
expectedfps = 60 # Expected FPS with running functions tested effects removed from current FPS
def GoodFPS(func="", factor=1, boolean=True, traceback=False, ):
2024-08-27 14:34:26 +02:00
settings = bge.logic.globalDict.get("settings", {})
2024-07-13 15:15:50 +02:00
2024-08-27 14:34:26 +02:00
2024-07-13 15:15:50 +02:00
global targetFPS
# Recording the framerate into buffer
# This buffer will be used to regulate the target FPS
fps = bge.logic.getAverageFrameRate()
frame = round(bge.logic.getRealTime(), 2)
camspeed = min(CameraSpeed(), 1)
global fpshistory
fpshistory[frame] = fps
# Limiting execution based on camera speed.
if boolean and numpy.random.choice([True, False], p=[camspeed, 1-camspeed]):
return False
# Setting the target fps
if frame > 5:# and fps > targetFPS:
2024-08-27 14:34:26 +02:00
if not settings.get("autofps"):
targetFPS = bge.logic.getLogicTicRate()
else:
targetFPS = max(fpshistory.values())
targetFPS *= 1.1
if not boolean and int(frame) % 30 == 0:
bge.logic.setLogicTicRate(int(targetFPS))
2024-07-13 15:15:50 +02:00
# Slicing buffer to contain only last 300 records
newfpshistory = {}
for i in list(fpshistory.keys())[-300:]:
newfpshistory[i] = fpshistory[i]
fpshistory = newfpshistory
# Calculating the fraction ( of how much are we within the target FPS )
fraction = ( sum( min(f, targetFPS) for f in fpshistory.values()) / len(fpshistory) ) / targetFPS
# Testing FPS impacts of various functions to see
# how much to limit them.
global FPStargets
global testFPS
global testingTarget
global testingTargetFrame
2024-08-27 14:34:26 +02:00
# Controlling the amount of how aggressively this function is executed.
optset = settings.get("opt", 1.0)
choice = numpy.random.choice([False, True], p=[optset, 1-optset])
if choice:
if boolean: return True
else: return 1.0
2024-07-13 15:15:50 +02:00
# This runs when the target for testing is already setup
if testingTarget:
# If we have some decrease in performace, we calculate how much it effects the FPS
if frame > testingTargetFrame and fps+1 < testFPS:
FPStargets[testingTarget] = {"fraction":fps / testFPS, "frame":frame}
testingTarget = False
# Otherwise we cancel the test
elif frame -2 > testingTargetFrame:
testingTarget = False
# While we testing, we want to return False, so nothing else will effect the FPS
if boolean: return False
else: return 0.0
# This runs to start the test
if ( func and func not in FPStargets and 10 < frame or FPStargets.get(func, {}).get("frame", frame) + 60 < frame ) and fraction > factor:
testFPS = fps
testingTarget = func
testingTargetFrame = frame
# And we want to return True so the function will get launched.
if boolean: return True
else: return 1.0
# Limiting functions based on the tests
global limitframe
global expectedfps
# Findinf the limiting factor
try:
targetFactor = min(n.get("fraction") for n in FPStargets.values()) * factor
except:
targetFactor = factor
2024-08-27 14:34:26 +02:00
2024-07-13 15:15:50 +02:00
# Limiting
if expectedfps * FPStargets.get(func, {}).get("fraction", 1) < targetFPS * targetFactor:
if boolean: return False
# If we allow the function to run
# we want to recalculate the expected FPS
# based on the tests
if limitframe != frame:
# If the tests data is flawed
if int(expectedfps) > int(fps)+2:
FPStargets = {}
# Calculating expected FPS
expectedfps = fps*FPStargets.get(func, {}).get("fraction", 1)
limitframe = frame
else:
expectedfps *= FPStargets.get(func, {}).get("fraction", 1)
# Returning
if boolean:
return True#fraction > factor
else:
return fraction
def Address(location, precision):
# This function will return an adress of any given location
# Which could be used to compare between objects to see if they
# are close enough together.
ret = ""
for axis in location[:2]:
ret = ret + str(round(axis/precision))+":"
return ret
def Surround(location, precision, camera=None):
# This function will give a list of addresses around a certain point.
ret = []
ORL = []
addedtypes = []
for axis in location[:2]:
ORL.append(round(axis/precision))
ret.append(Address(ORL, 1))
if not camera:
# X
if (location[0]/precision) - round(location[0]/precision) < 0.5:
ret.append(Address([ORL[0]-1, ORL[1]], 1))
addedtypes.append("-x")
elif (location[0]/precision) - round(location[0]/precision) > 0.5:
ret.append(Address([ORL[0]+1, ORL[1]], 1))
addedtypes.append("+x")
# Y
if (location[1]/precision) - round(location[1]/precision) < 0.5:
ret.append(Address([ORL[0], ORL[1]-1], 1))
addedtypes.append("-y")
elif (location[1]/precision) - round(location[1]/precision) > 0.5:
ret.append(Address([ORL[0], ORL[1]+1], 1))
addedtypes.append("+y")
# Diagonals
if "+x" in addedtypes and "+y" in addedtypes:
ret.append(Address([ORL[0]+1, ORL[1]+1], 1))
elif "-x" in addedtypes and "-y" in addedtypes:
ret.append(Address([ORL[0]-1, ORL[1]-1], 1))
elif "-x" in addedtypes and "+y" in addedtypes:
ret.append(Address([ORL[0]-1, ORL[1]+1], 1))
elif "+x" in addedtypes and "-y" in addedtypes:
ret.append(Address([ORL[0]+1, ORL[1]-1], 1))
else:
if r(70) >= camera[2] >= r(-70):
ret.append(Address([ORL[0], ORL[1]+1], 1))
if r(25) >= camera[2] >= r(-115):
ret.append(Address([ORL[0]+1, ORL[1]+1], 1))
if r(-20) >= camera[2] >= r(-160):
ret.append(Address([ORL[0]+1, ORL[1]], 1))
if r(-65) >= camera[2] >= r(-180) or r(180) > camera[2] >= r(155):
ret.append(Address([ORL[0]+1, ORL[1]-1], 1))
if r(-110) >= camera[2] >= r(-180) or r(180) > camera[2] >= r(110):
ret.append(Address([ORL[0], ORL[1]-1], 1))
if r(-155) >= camera[2] >= r(180) or r(180) > camera[2] >= r(65):
ret.append(Address([ORL[0]-1, ORL[1]-1], 1))
if r(160) >= camera[2] >= r(20):
ret.append(Address([ORL[0]-1, ORL[1]], 1))
if r(115) >= camera[2] >= r(-25):
ret.append(Address([ORL[0]-1, ORL[1]+1], 1))
return ret
previousSurround = {}
def SurroundChanged(key, surround):
# This returns whether surround was changed between frames
if key not in previousSurround:
previousSurround[key] = surround.copy()
changed = surround != previousSurround[key]
previousSurround[key] = surround.copy()
return changed
def RegisterObject(object, precision, delete=True):
# This will move an object from scene.objects into
# chunks. So that they could be stored without
# effecting the BGE Depsgraph performance.
# Creating chunk
addr = Address(object.position, precision)
if addr not in chunks:
chunks[addr] = {"loaded":False,
"objects":[]}
# Adding properties
virtualObject = {}
virtualObject["name"] = object.name
virtualObject["position"] = object.position.copy()
virtualObject["orientation"] = object.orientation.to_euler()
virtualObject["scaling"] = object.scaling.copy()
for i in object.blenderObject.game.properties:
virtualObject[i.name] = i.value
# Adding the object to the chunk
chunks[addr]["objects"].append(virtualObject)
# Deleting real object
if delete:
object.endObject()
# Returning
return virtualObject
def SortByDistance(l, cam, withdistance=False):
# This sorts a chunk by the distance
distanced = []
for n, i in enumerate(l):
d = cam.getDistanceTo(i["position"])
distanced.append([d, n, i])
distanced = sorted(distanced)
if withdistance:
return distanced
ret = []
for i in distanced:
ret.append(i[2])
return ret
def UpdateScene(camobject, precision, camera=None):
# This function will update the scene. According to the camera
# view.
location = camobject.position
# Getting addresses
addrs = Surround(location, precision, camera)
# Checking if any of addresses are not up to date.
for addr in chunks:
# Delete object if address not in view
# It is done in a separate loop and first,
# because we might need elements from it
# to spawn later. Otherwise we might cache
# way too many objects into memory.
if addr not in addrs and chunks[addr]["loaded"]:
for object in chunks[addr]["objects"]:
if object.get("object"):
Reuse.Delete(object["object"])
object["object"] = None
chunks[addr]["loaded"] = False
# Breaking to skip to next frame and
# re-evaluate whether the FPS is good
return
lookthroughchunks = chunks
if addrs[0] in chunks:
lookthroughchunks = [chunks[addrs[0]]]+list(chunks.keys())
# Fixing random items at the chunks.
for i in range(5):
addr = random.choice(list(chunks.keys()))
item = random.choice(chunks[addr]["objects"])
if item.get("broken"):
item["broken"] = False
print(consoleForm(item["name"]), "Fixed")
for addr in chunks:
# Create objects when addres in view
if addr in addrs and not chunks[addr]["loaded"]:
for object in SortByDistance(chunks[addr]["objects"], camobject):
if not object.get("object") and not object.get("broken"):
object["object"] = Reuse.Create(object["name"])
object["object"].position = object["position"]
object["object"].orientation = object["orientation"]
object["object"].scaling = object["scaling"]
# Some objects have a height adjustment value
object["object"].position[2] += object["object"].get("movez", 0)
# Some effects require a reference point
object["object"]["spawned_by"] = object
# Some objects need dynamics to be suspended
if object.get("suspendDynamics"):
object["object"].suspendDynamics(True)
# Some objects might change mesh during game
if object["object"].get("good"):
object["object"].replaceMesh(object["object"]["good"])
chunks[addr]["loaded"] = True
# Breaking to skip to next frame and
# re-evaluate whether the FPS is good
return
# Level Of Details for spawn objects
# This is needed because normal LOD system
# with those objects sometimes gives a
# segmentation fault.
elif addr in addrs and chunks[addr]["loaded"]:
sortedObjects = SortByDistance(chunks[addr]["objects"], camobject, True)
# closestUnspawned = [1000000, 0, {}]
# for object in sortedObjects:
# if not object[2].get("object") and object[0] < closestUnspawned[0]:
# closestUnspawned = object
#
# if Reuse.reuse.get(closestUnspawned[2].get("name")):
# closestUnspawned[2]["object"] = Reuse.Create(closestUnspawned[2].get("name"))
# closestUnspawned[2]["object"].position = closestUnspawned[2]["position"]
# closestUnspawned[2]["object"].orientation = closestUnspawned[2]["orientation"]
# closestUnspawned[2]["object"].scaling = closestUnspawned[2]["scaling"]
#
# # Some objects have a height adjustment value
# closestUnspawned[2]["object"].position[2] += closestUnspawned[2]["object"].get("movez", 0)
#
# # Some effects require a reference point
# closestUnspawned[2]["object"]["spawned_by"] = closestUnspawned[2]
#
# # Some objects need dynamics to be suspended
# if closestUnspawned[2].get("suspendDynamics"):
# closestUnspawned[2]["object"].suspendDynamics(True)
for distance, n, object in sortedObjects:
#
# if object["name"] == closestUnspawned[2].get("name") and distance > closestUnspawned[0] and object.get("object"):
# Reuse.Delete(object["object"])
# object["object"] = None
if "lods" in object and object.get("object"):
distance = object["object"].getDistanceTo(location)
for lod in object["lods"]:
lodDistance = object["lods"][lod]
if int(distance) <= lodDistance:
break
if object["object"].name != lod:
Reuse.Delete(object["object"])
object["object"] = Reuse.Create(lod)
object["object"].position = object["position"]
object["object"].orientation = object["orientation"]
object["object"].scaling = object["scaling"]
# Some objects have a height adjustment value
object["object"].position[2] += object["object"].get("movez", 0)
# Some effects require a reference point
object["object"]["spawned_by"] = object
# Some objects need dynamics to be suspended
if object.get("suspendDynamics"):
object["object"].suspendDynamics(True)
taskschedule = {}
def ScheduleTask(taskname, FPSfactor, task, *args):
# This function adds a function to the schedule
taskObject = {"FPSfactor":FPSfactor,
"task":task,
"args":args}
taskschedule[taskname] = taskObject
def ExecuteScheduled():
# This function executes a scheduled task
# for taskname in taskschedule:
# print("Scheduled:", taskname)
# print()
# Trying to execute something
for taskname in taskschedule:
taskObject = taskschedule[taskname]
if GoodFPS("[Scheduled] "+taskname, taskObject["FPSfactor"]):
del taskschedule[taskname]
taskObject["task"](*taskObject["args"])
return
def Force(value):
# This function adjusts acceleration
# to fps
return value * ( ToFPS() ** 2 )
def Velocity(value):
# This function adjusts velocity
# to fps
return value * ToFPS()
def PrintTime():
# This function is used for testing
print(bge.logic.getRealTime())