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() # 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, ): settings = bge.logic.globalDict.get("settings", {}) 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: 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)) # 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 # 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 # 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 # 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())