Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- Game Framework
- */
- include "io", "string", "fmt", "math";
- global BIGNUM = 1e+32;
- /* Utility functions */
- function sign( x ){ return if( x == 0, 0, if( x < 0, -1, 1 ) ); }
- function normalize_angle( x ){ x = x % ( M_PI * 2 ); return if( x < 0, x + M_PI*2, x ); }
- function saturate( x ){ return if( x < 0, 0, if( x > 1, 1, x ) ); }
- function smoothstep( x ){ return x * x * ( 3 - 2 * x ); }
- function smoothlerp_oneway( t, a, b ){ if( b == a ) return 1.0; return smoothstep( saturate( ( t - a ) / ( b - a ) ) ); }
- function smoothlerp_range( t, a, b, c, d ){ return smoothlerp_oneway( t, a, b ) * smoothlerp_oneway( t, d, c ); }
- global GameFramework =
- {
- Debug = false,
- };
- /*
- Bigger utility functions
- */
- function Config_Load( file, defaults )
- {
- out = null;
- pcall(function() use(file, defaults, out)
- {
- data = io_file_read( file );
- if( !data )
- return "failed to read the file";
- data = string_replace( data, ["\r\n","\r"], "\n" );
- entries = string_explode( data, "\n" );
- out = clone( defaults );
- foreach( eid, entry : entries )
- {
- keyval = string_explode( entry, "=" );
- if( keyval.size != 2 )
- return "error in format, entry #"$(eid+1);
- key = string_trim( keyval[0] );
- value = string_trim( keyval[1] );
- out[ key ] = value;
- }
- });
- return out;
- }
- /*
- GameLoop
- - create( fn, mindt, maxdt )
- + !call( func )
- */
- global GameLoop = {};
- function GameLoop.create( fn, mindt, maxdt )
- {
- lasttime = ftime();
- mindt ||= 0.0;
- maxdt ||= 1.0 / 15.0;
- stickydelta = 0.016;
- return function() use( fn, mindt, maxdt, lasttime, stickydelta )
- {
- curtime = ftime();
- delta = curtime - lasttime;
- lasttime = curtime;
- if( delta > 1.0/15.0 )
- delta = 1.0/15.0;
- if( GameFramework.Debug && abs( stickydelta - delta ) > 1.0 / 120.0 )
- println( "GFW DEBUG INFO: major deviation in delta time: "${ stickydelta = stickydelta, delta = delta } );
- stickydelta = stickydelta * 0.95 + delta * 0.05;
- fn( stickydelta, lasttime );
- if( mindt > 0 )
- {
- sleeptime = mindt - ( ftime() - lasttime );
- if( sleeptime > 0 )
- ;// sleep( sleeptime * 1000 );
- }
- };
- }
- /*
- InputKeys
- - create()
- > clear()
- > add_range( from, count )
- > add_list( list )
- > advance()
- > set( key, isdown )
- > get( key )
- > getprev( key )
- > is_pressed( key )
- > is_released( key )
- */
- global InputKeys =
- {
- keys = {},
- prevkeys = {},
- };
- function InputKeys.create()
- {
- data =
- {
- keys = {},
- prevkeys = {},
- };
- return class( data, InputKeys );
- }
- function InputKeys.clear()
- {
- this.keys = {};
- this.prevkeys = {};
- }
- function InputKeys.add_range( from, count )
- {
- for( i = from; i < from + count; ++i )
- {
- this.keys[ i ] = 0.0;
- this.prevkeys[ i ] = 0.0;
- }
- }
- function InputKeys.add_list( list )
- {
- foreach( i : list )
- {
- this.keys[ i ] = 0.0;
- this.prevkeys[ i ] = 0.0;
- }
- }
- function InputKeys.advance()
- {
- foreach( i, v : this.keys )
- this.prevkeys[ i ] = v;
- }
- function InputKeys.set( key, amount ){ this.keys[ key ] = amount; }
- function InputKeys.get( key ){ return this.keys[ key ]; }
- function InputKeys.getprev( key ){ return this.keys[ key ]; }
- function InputKeys.is_pressed( key ){ return this.keys[ key ] && !this.prevkeys[ key ]; }
- function InputKeys.is_released( key ){ return !this.keys[ key ] && this.prevkeys[ key ]; }
- /*
- ActionMap
- - create()
- + add_action( action )
- + remove_action( action )
- + advance()
- + set( action, amount )
- + get( action )
- + getprev( action )
- + is_pressed( action )
- + is_released( action )
- */
- global ActionMap = {};
- function ActionMap.create()
- {
- data =
- {
- actions = {},
- prevactions = {},
- keys_to_actions = {},
- };
- return class( data, ActionMap );
- }
- function ActionMap.add_action( action )
- {
- this.actions[ action ] = this.prevactions[ action ] = 0.0;
- }
- function ActionMap.remove_action( action )
- {
- unset( this.actions, action );
- unset( this.prevactions, action );
- }
- function ActionMap.has_binding( action ){ return get_values( this.keys_to_actions ).find( action ) !== null; }
- function ActionMap.add_binding( action, key ){ this.keys_to_actions[ key ] = action; }
- function ActionMap.add_default_binding( action, key ){ if( !this.has_binding( action ) ) this.add_binding( action, key ); }
- function ActionMap.remove_binding( action, key ){ if( this.keys_to_actions[ key ] == action ) unset( this.keys_to_actions, key ); }
- function ActionMap.remove_all_bindings( action )
- {
- foreach( k, a : this.keys_to_actions )
- {
- if( a == action )
- {
- this.keys_to_actions[ k ] = null;
- break;
- }
- }
- this.keys_to_actions = dict_filter( this.keys_to_actions );
- }
- function ActionMap.advance()
- {
- foreach( i, v : this.actions )
- this.prevactions[ i ] = v;
- }
- function ActionMap.set( key, amount )
- {
- if( isset( this.keys_to_actions, key ) )
- this.actions[ this.keys_to_actions[ key ] ] = toreal( amount );
- }
- function ActionMap.get( action ){ return this.actions[ action ]; }
- function ActionMap.getprev( action ){ return this.prevactions[ action ]; }
- function ActionMap.is_pressed( action ){ return this.actions[ action ] && !this.prevactions[ action ]; }
- function ActionMap.is_released( action ){ return !this.actions[ action ] && this.prevactions[ action ]; }
- /*
- EntityBuilder
- - clear()
- - scan( folder )
- - build( type, params )
- */
- global EntityBuilder =
- {
- entity_types = {},
- };
- function EntityBuilder._load_error( file, type, error )
- {
- ERROR( string_format( "ENTITY BUILDER: error while loading file {1}, type {2} - {3}", file, type, error ) );
- }
- function EntityBuilder._build_error( type, error )
- {
- ERROR( string_format( "ENTITY BUILDER: error while creating entity of type {1} - {2}", type, error ) );
- }
- function EntityBuilder.clear()
- {
- this.entity_types = {};
- }
- function EntityBuilder._prepare( file, type, data )
- {
- if( !isset( data, "create" ) ) this._load_error( file, type, "missing 'create' function" );
- if( !is_callable( data.create ) ) this._load_error( file, type, "'create' not callable" );
- if( !isset( data, "defaults" ) )
- data.defaults = {};
- }
- function EntityBuilder.scan( folder )
- {
- foreach( is_real, item : io_dir( folder ) )
- {
- if( !is_real )
- continue;
- fullpath = folder $ "/" $ item;
- new_ents = eval_file( fullpath );
- foreach( new_ent_name, new_ent_data : new_ents )
- {
- this._prepare( fullpath, new_ent_name, new_ent_data );
- }
- this.entity_types = get_merged( this.entity_types, new_ents );
- }
- }
- function EntityBuilder.build( type, params )
- {
- if( !isset( this.entity_types, type ) )
- return this._build_error( type, "type is not loaded" );
- info = this.entity_types[ type ];
- params = get_merged( info.defaults, params );
- return info.create( params );
- }
- /*
- GameWorld
- - create()
- + add_entity( entity )
- + remove_entity( id )
- + remove_all_entities()
- + get_entity_by_id( id )
- + tick( delta )
- */
- global GameWorld = {};
- function GameWorld.create()
- {
- data =
- {
- // DATA
- entities = {},
- in_tick = false,
- // CACHE
- z_order = [],
- z_indices = [],
- to_add = [],
- to_remove = [],
- // INDEXING
- auto_id = 1,
- // UTILITY
- efn = function(){},
- };
- return class( data, GameWorld );
- }
- function GameWorld._real_add_entity( entity )
- {
- entity.id = this.auto_id++;
- if( !isset( entity, "z_index" ) )
- entity.z_index = 0;
- if( !isset( entity, "tick" ) ) entity.tick = this.efn;
- if( !isset( entity, "draw" ) ) entity.draw = this.efn;
- if( !isset( entity, "draw_ui" ) ) entity.draw_ui = this.efn;
- this.entities[ entity.id ] = entity;
- if( isset( entity, "added" ) ) entity.added( GameWorld );
- }
- function GameWorld._real_remove_entity( eid )
- {
- if( isset( this.entities, eid ) )
- {
- if( isset( this.entities[ eid ], "removed" ) )
- this.entities[ eid ].removed( GameWorld );
- unset( this.entities, eid );
- }
- }
- function GameWorld._process_queues()
- {
- foreach( eid : this.to_remove )
- this._real_remove_entity( eid );
- this.to_remove.clear();
- foreach( entity : this.to_add )
- this._real_add_entity( entity );
- this.to_add.clear();
- }
- function GameWorld.add_entity( entity )
- {
- // expect to be used inside tick
- if( this.in_tick )
- {
- this.to_add.push( entity );
- }
- else
- {
- this._real_add_entity( entity );
- }
- }
- function GameWorld.remove_entity( eid )
- {
- // expect to be used inside tick
- // cannot search to_add because there are no IDs assigned yet
- if( this.in_tick )
- {
- this.to_remove.push( eid );
- }
- else
- {
- this._real_remove_entity( eid );
- }
- }
- function GameWorld.remove_all_entities()
- {
- // clone to avoid modification on iteration
- foreach( eid, : clone(this.entities) )
- this.remove_entity( eid );
- }
- function GameWorld.get_entity_by_id( eid )
- {
- if( isset( this.entities, eid ) )
- return this.entities[ eid ];
- return null;
- }
- function GameWorld.tick( delta )
- {
- this._process_queues();
- this.in_tick = true;
- this.z_indices.clear();
- this.z_order.clear();
- foreach( entity : this.entities )
- {
- if( delta > 0 )
- entity.tick( delta );
- this.z_indices.push( entity.z_index );
- this.z_order.push( entity );
- }
- this.z_order.sort_mapped( this.z_indices );
- foreach( entity : this.z_order )
- entity.draw();
- this.in_tick = false;
- }
- function GameWorld.tick_nodraw( delta )
- {
- this._process_queues();
- if( delta > 0 )
- {
- this.in_tick = true;
- foreach( entity : this.entities )
- {
- entity.tick( delta );
- }
- this.in_tick = false;
- }
- }
- function GameWorld.send_message( eid, message, param )
- {
- if( !isset( this.entities, eid ) )
- return false;
- ent = this.entities[ eid ];
- if( !isset( ent, message ) )
- return false;
- ent.(message)( param );
- return true;
- }
- function GameWorld.draw_ui( w, h )
- {
- foreach( entity : this.z_order )
- entity.draw_ui( w, h );
- }
- /*
- AABBHash
- [aabb => min_x, min_y, max_x, max_y]
- - create( cell_width, cell_height )
- + cleanup()
- + add( aabb, item )
- + remove( aabb, item )
- + find_point( x, y )
- + find_range( aabb )
- + for_each( aabb, func, create = false )
- + point_to_cell( x, y ) => (cx,cy)
- + cell_id( x, y )
- */
- global AABBHash = {};
- function AABBHash.create( cell_width, cell_height )
- {
- if( cell_width <= 0.0 || cell_height <= 0.0 )
- return ERROR( "AABBHash: cell size must be positive" );
- data =
- {
- cell_width = toreal( cell_width ),
- cell_height = toreal( cell_height ),
- hash = {},
- };
- return class( data, AABBHash );
- }
- function AABBHash.cleanup()
- {
- // remove empty arrays from hash table
- this.hash = dict_filter( this.hash );
- }
- function AABBHash.add( min_x, min_y, max_x, max_y, item )
- {
- this.for_each( min_x, min_y, max_x, max_y, function( cell ) use( item )
- {
- if( cell.find( item ) === null )
- cell.push( item );
- }, true );
- }
- function AABBHash.remove( min_x, min_y, max_x, max_y, item )
- {
- this.for_each( min_x, min_y, max_x, max_y, function( cell ) use( item )
- {
- cell.remove( item );
- });
- }
- function AABBHash.find_point( x, y )
- {
- (cx,cy) = this.point_to_cell( x, y );
- id = this.cell_id( cx, cy );
- if( isset( this.hash, id ) )
- return this.hash[ id ];
- return [];
- }
- function AABBHash.find_range( min_x, min_y, max_x, max_y )
- {
- out = [];
- this.for_each( min_x, min_y, max_x, max_y, function( cell ) use( out )
- {
- out = get_concat( out, cell );
- });
- return out.unique();
- }
- function AABBHash.intersects( min_x, min_y, max_x, max_y )
- {
- return min_x < this.max_x && this.min_x <= max_x
- && min_y < this.max_y && this.min_y <= max_y;
- }
- function AABBHash.for_each( min_x, min_y, max_x, max_y, func, create )
- {
- create ||= false;
- (mincx,mincy) = this.point_to_cell( min_x, min_y );
- (maxcx,maxcy) = this.point_to_cell( max_x, max_y );
- for( y = mincy; y <= maxcy; ++y )
- {
- for( x = mincx; x <= maxcx; ++x )
- {
- id = this.cell_id( x, y );
- if( isset( this.hash, id ) )
- func( this.hash[ id ] );
- else if( create )
- {
- arr = [];
- func( arr );
- this.hash[ id ] = arr;
- }
- }
- }
- }
- function AABBHash.point_to_cell( x, y )
- {
- x /= this.cell_width;
- y /= this.cell_height;
- x = floor( x );
- y = floor( y );
- return x, y;
- }
- function AABBHash.cell_id( x, y )
- {
- // return x$'|'$y;
- return fmt_pack( "ll", x, y );
- }
- /*
- AStarPathfinder
- - create( links, cost_function = <1>, data_set = null )
- + find_path( from_id, to_id )
- */
- global AStarPathfinder = {};
- function AStarPathfinder.create( links, cost_function, data_set )
- {
- cost_function ||= function(){ return 1; };
- node_links = {};
- node_pair_links = {};
- foreach( link : links )
- {
- n0 = link[0];
- n1 = link[1];
- if( !isset( node_links, n0 ) )
- node_links[ n0 ] = [ link ];
- else
- node_links[ n0 ].push( link );
- if( !isset( node_links, n1 ) )
- node_links[ n1 ] = [ link ];
- else
- node_links[ n1 ].push( link );
- node_pair_links[ fmt_pack( "ll", n0, n1 ) ] = link;
- node_pair_links[ fmt_pack( "ll", n1, n0 ) ] = link;
- }
- data =
- {
- links = links,
- cost_function = cost_function,
- data_set = data_set,
- node_links = node_links,
- node_pair_links = node_pair_links,
- };
- return class( data, AStarPathfinder );
- }
- function AStarPathfinder.find_path( from_id, to_id, from_item, to_item )
- {
- if( from_id == to_id )
- return [];
- cost_function = this.cost_function;
- data_set = this.data_set;
- closed_set = {};
- open_set = {}; open_set[ from_id ] = true;
- came_from = {};
- g_score = {};
- f_score = {};
- g_score[ from_id ] = 0;
- f_score[ from_id ] = cost_function( from_item, to_item );
- while( open_set )
- {
- // current = item from f_score with lowest cost
- lowest = BIGNUM;
- current = -1;
- foreach( fitem ,: open_set )
- {
- score = f_score[ fitem ];
- if( score < lowest )
- {
- current = fitem;
- lowest = score;
- }
- }
- // reached destination
- if( current == to_id )
- {
- prev = to_id;
- out = [];
- for(;;)
- {
- next = came_from[ prev ];
- out.push( this.node_pair_links[ fmt_pack( "ll", prev, next ) ] );
- if( next == from_id )
- break;
- prev = next;
- }
- return out.reverse();
- }
- // mark node as checked
- unset( open_set, current );
- closed_set[ current ] = true;
- // check the neighbors
- if( isset( this.node_links, current ) )
- {
- foreach( neighbor_obj : this.node_links[ current ] )
- {
- neighbor = neighbor_obj[ neighbor_obj[0] == current ];
- pred_g_score = g_score[ current ] + cost_function( if( current == from_id, from_item, data_set[ current ] ), data_set[ neighbor ] );
- pred_f_score = pred_g_score + cost_function( data_set[ neighbor ], to_item );
- if( isset( closed_set, neighbor ) && pred_f_score >= f_score[ neighbor ] )
- continue;
- if( !isset( open_set, neighbor ) || pred_f_score < f_score[ neighbor ] )
- {
- came_from[ neighbor ] = current;
- g_score[ neighbor ] = pred_g_score;
- f_score[ neighbor ] = pred_f_score;
- open_set[ neighbor ] = true;
- }
- }
- }
- }
- return null;
- }
- /*
- NavMeshPathfinder
- - create( polygons, links )
- + find_path( from_x, from_y, to_x, to_y, char_width = 0 )
- - poly_to_aabb( poly ) => (min_x,min_y,max_x,max_y)
- + find_point_polygon( x, y )
- - cost_function( p1, p2, data(centers) )
- Notes:
- - link format: [ p1, p2, p1v1, p1v2, p2v1, p2v2, point1, point2 ]
- - find_path returns a list of points, each between its link line segment endpoints ..
- - .. proper corner handling can be done by looking ahead, forming a triangle and checking where the path will turn ..
- - .. ending point (to_x,to_y) can be added manually if necessary - after all, it is passed to the function
- */
- global NavMeshPathfinder = {};
- function NavMeshPathfinder.create( polygons, links )
- {
- centers = [].reserve( polygons.size );
- foreach( poly : polygons )
- {
- center = vec2(0,0);
- foreach( vertex : poly )
- center += vertex;
- center /= poly.size;
- centers.push( center );
- }
- bbhash = AABBHash.create( 32, 32 );
- foreach( pid, poly : polygons )
- {
- (min_x,min_y,max_x,max_y) = NavMeshPathfinder.poly_to_aabb( poly );
- bbhash.add( min_x, min_y, max_x, max_y, pid );
- }
- astar = AStarPathfinder.create( links, NavMeshPathfinder.cost_function, centers );
- data =
- {
- bbhash = bbhash,
- pathfinder = astar,
- polygons = polygons,
- centers = centers,
- links = links,
- };
- return class( data, NavMeshPathfinder );
- }
- function NavMeshPathfinder.find_path( from_x, from_y, to_x, to_y, char_radius )
- {
- from_id = this.find_point_polygon( from_x, from_y );
- to_id = this.find_point_polygon( to_x, to_y );
- if( from_id === null || to_id === null )
- return null;
- from_v2 = vec2(from_x,from_y);
- to_v2 = vec2(to_x,to_y);
- asp = this.pathfinder.find_path( from_id, to_id, from_v2, to_v2 );
- if( asp === null )
- return null;
- char_radius ||= 0;
- out = [ from_v2 ];
- foreach( pl : asp )
- {
- out.push( ( pl[ 6 ] + pl[ 7 ] ) / 2 );
- }
- out.push( to_v2 );
- iters = 3;
- last = out.size - 2;
- while( iters --> 0 )
- {
- for( i = 1; i <= last; ++i )
- {
- pi = i - 1;
- // get link limits
- lep1 = asp[pi][6];
- lep2 = asp[pi][7];
- t = ( lep2 - lep1 ).normalized;
- n = t.perp;
- dpn = vec2_dot( lep1, n );
- dp1 = vec2_dot( lep1, t );
- dp2 = vec2_dot( lep2, t );
- dcp = vec2_dot( out[i], t );
- dcpPrev = vec2_dot( out[i-1], t );
- dcpNext = vec2_dot( out[i+1], t );
- ndpPrev = vec2_dot( out[i-1], n );
- ndpCurr = dpn;
- ndpNext = vec2_dot( out[i+1], n );
- // average and clamp
- sum = abs( ndpPrev - ndpCurr ) + abs( ndpNext - ndpCurr );
- factor = if( sum < 0.01, 0.5, abs( ndpPrev - ndpCurr ) / sum );
- dcp = dcpPrev * (1-factor) + dcpNext * factor;
- if( dcp < dp1 ) dcp = dp1;
- if( dcp > dp2 ) dcp = dp2;
- // pull out of walls if necessary
- fp = t * dcp + n * dpn;
- if( dcp <= dp1 + asp[pi][8].length * char_radius ) fp = asp[pi][6] + asp[pi][8] * char_radius;
- if( dcp >= dp2 - asp[pi][9].length * char_radius ) fp = asp[pi][7] + asp[pi][9] * char_radius;
- // push back
- out[i] = fp;
- }
- }
- out.pop();
- out.shift();
- return out;
- }
- function NavMeshPathfinder.poly_to_aabb( poly )
- {
- min_x = BIGNUM;
- max_x = -BIGNUM;
- min_y = BIGNUM;
- max_y = -BIGNUM;
- foreach( v : poly )
- {
- x = v.x;
- y = v.y;
- if( x < min_x ) min_x = x;
- if( x > max_x ) max_x = x;
- if( y < min_y ) min_y = y;
- if( y > max_y ) max_y = y;
- }
- return min_x, min_y, max_x, max_y;
- }
- function NavMeshPathfinder.find_point_polygon( x, y )
- {
- polys = this.bbhash.find_point( x, y );
- if( polys )
- {
- // find overlap
- foreach( pid : polys )
- {
- poly = this.polygons[ pid ];
- if( this.point_in_polygon( poly, x, y ) )
- return pid;
- }
- }
- return null;
- }
- function NavMeshPathfinder.point_in_polygon( poly, x, y )
- {
- if( poly.size < 3 )
- return false;
- tp = vec2(x,y);
- pp = poly.last;
- foreach( cp : poly )
- {
- enorm = ( cp - pp ).perp2.normalized;
- if( vec2_dot( enorm, tp ) > vec2_dot( enorm, cp ) + 0.001 )
- return false;
- pp = cp;
- }
- return true;
- }
- function NavMeshPathfinder.cost_function( p1, p2 )
- {
- return ( p1 - p2 ).length;
- }
- /*
- AIInfo
- - create()
- + put_entity( id, group, pos )
- + remove_entity( id )
- + emit_sound( type, timeout, radius, pos )
- + gather_visible( from, func )
- + gather_heard( from )
- SOUND [ pos, type, timeout, radius ]
- ENTITY [ pos, group ]
- */
- global AIInfo = {};
- function AIInfo.create()
- {
- data =
- {
- entities = {},
- sounds = [],
- };
- return class( data, AIInfo );
- }
- function AIInfo.put_entity( id, group, pos )
- {
- this.entities[ id ] = [ pos, group ];
- }
- function AIInfo.remove_entity( id )
- {
- unset( this.entities, id );
- }
- function AIInfo.emit_sound( pos, type, timeout, radius, id )
- {
- this.sounds.push([ pos, type, timeout, radius, id ]);
- }
- function AIInfo.gather_visible( from, func )
- {
- visible = {};
- foreach( eid, entity : this.entities )
- {
- factor = func( eid, entity, from );
- if( factor > 0 )
- visible[ eid ] = get_concat( [ factor ], entity );
- }
- return visible;
- }
- function AIInfo.gather_heard( from, mindist, ignore_ids )
- {
- heard = [].reserve( sqrt( this.sounds.size ) );
- foreach( sound : this.sounds )
- {
- dist = ( sound[0] - from ).length;
- if( dist < mindist ) continue;
- if( dist > sound[3] ) continue;
- if( ignore_ids && ignore_ids.find( sound[4] ) !== null ){ continue; }
- power = 1 - dist / sound[3];
- heard.push([
- power,
- sound[0],
- sound[1],
- sound[2],
- sound[3],
- sound[4]
- ]);
- }
- return heard;
- }
- function AIInfo.tick( delta )
- {
- foreach( sid, sound : this.sounds )
- {
- if( ( sound[1] -= delta ) < 0 )
- {
- this.sounds[ sid ] = null;
- continue;
- }
- }
- this.sounds = array_filter( this.sounds );
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement