import retro platformer controller and mod player
This commit is contained in:
parent
992bdac15e
commit
40d2372f2b
|
@ -0,0 +1,23 @@
|
|||
"""
|
||||
Godot Mod Player Plugin by arlez80 (Yui Kinomoto)
|
||||
"""
|
||||
|
||||
tool
|
||||
extends EditorPlugin
|
||||
|
||||
func _enter_tree( ):
|
||||
#self.add_custom_type( "GodotModPlayer", "Spatial", preload("ModPlayer.gd"), preload("icon.png") )
|
||||
pass
|
||||
|
||||
func _exit_tree( ):
|
||||
#self.remove_custom_type( "GodotModPlayer" )
|
||||
pass
|
||||
|
||||
func has_main_screen():
|
||||
return true
|
||||
|
||||
func make_visible( visible:bool ):
|
||||
pass
|
||||
|
||||
func get_plugin_name( ):
|
||||
return "Godot Mod Player"
|
|
@ -0,0 +1,290 @@
|
|||
"""
|
||||
MOD reader by あるる(きのもと 結衣) @arlez80
|
||||
|
||||
MIT License
|
||||
"""
|
||||
|
||||
class_name Mod
|
||||
|
||||
const period_table:PoolIntArray = PoolIntArray([
|
||||
1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907,
|
||||
856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453,
|
||||
428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226,
|
||||
214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113,
|
||||
107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57,
|
||||
])
|
||||
|
||||
enum ModFlags {
|
||||
LINEAR_FREQUENCY_TABLE = 1,
|
||||
}
|
||||
|
||||
enum ModWaveFormType {
|
||||
SINE_WAVEFORM = 0,
|
||||
SAW_WAVEFORM = 1,
|
||||
SQUARE_WAVEFORM = 2,
|
||||
RAMDOM_WAVEFORM = 3,
|
||||
REV_SAW_WAVEFORM = 4,
|
||||
}
|
||||
|
||||
enum ModLoopType {
|
||||
NO_LOOP = 0,
|
||||
FORWARD_LOOP = 1,
|
||||
PING_PONG_LOOP = 2,
|
||||
}
|
||||
|
||||
class ModData:
|
||||
var module_name:String
|
||||
var tracker_name:String
|
||||
var version:int
|
||||
var flags:int = 0
|
||||
|
||||
var song_length:int
|
||||
var unknown_number:int
|
||||
var channel_count:int
|
||||
var song_positions:Array # int
|
||||
var magic:String
|
||||
var patterns:Array # ModPattern[][]
|
||||
var instruments:Array # ModInstrument[]
|
||||
|
||||
var init_tick:int = 6
|
||||
var init_bpm:int = 125
|
||||
var restart_position:int = 0
|
||||
|
||||
class ModPatternNote:
|
||||
var key_number:int
|
||||
var note:int
|
||||
var instrument:int
|
||||
var volume:int = 0
|
||||
var effect_command:int
|
||||
var effect_param:int
|
||||
|
||||
class ModInstrument:
|
||||
var name:String
|
||||
var samples:Array # ModSample[96]
|
||||
var volume_envelope:ModEnvelope
|
||||
var panning_envelope:ModEnvelope
|
||||
var vibrato_type:int = -1 # ModWaveFormType
|
||||
var vibrato_speed:int = -1
|
||||
var vibrato_depth:int = -1
|
||||
var vibrato_depth_shift:int = -1
|
||||
var volume_fadeout:int = -1
|
||||
|
||||
class ModSample:
|
||||
var data:PoolByteArray
|
||||
var name:String
|
||||
var length:int
|
||||
var loop_start:int
|
||||
var loop_length:int
|
||||
var volume:int
|
||||
var finetune:int
|
||||
var loop_type:int = ModLoopType.FORWARD_LOOP # set(ModLoopType)
|
||||
var bit:int = 8 # 8 or 16
|
||||
var panning:int = -1
|
||||
var relative_note:int = 0
|
||||
|
||||
class ModEnvelope:
|
||||
var points:Array # ModEnvelopePoint[12]
|
||||
var point_count:int
|
||||
var sustain_point:int
|
||||
var loop_start_point:int
|
||||
var loop_end_point:int
|
||||
|
||||
var enabled:bool
|
||||
var sustain_enabled:bool
|
||||
var loop_enabled:bool
|
||||
|
||||
func set_flag( f:int ):
|
||||
self.enabled = ( f & 1 ) != 0
|
||||
self.sustain_enabled = ( f & 2 ) != 0
|
||||
self.loop_enabled = ( f & 4 ) != 0
|
||||
|
||||
class ModEnvelopePoint:
|
||||
var frame:int
|
||||
var value:int
|
||||
|
||||
class ModParseResult:
|
||||
var error:int = OK
|
||||
var data:ModData = null
|
||||
|
||||
func _init( ):
|
||||
pass
|
||||
|
||||
"""
|
||||
ファイルから読み込み
|
||||
|
||||
@param path ファイルパス
|
||||
@return 読んだ Modファイルデータ
|
||||
"""
|
||||
func read_file( path:String ) -> ModParseResult:
|
||||
var f:File = File.new( )
|
||||
var result: = ModParseResult.new( )
|
||||
|
||||
var err:int = f.open( path, f.READ )
|
||||
if err != OK:
|
||||
result.error = err
|
||||
return result
|
||||
|
||||
var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
|
||||
stream.set_data_array( f.get_buffer( f.get_len( ) ) )
|
||||
stream.big_endian = true
|
||||
f.close( )
|
||||
|
||||
result.data = self._read( stream )
|
||||
return result
|
||||
|
||||
"""
|
||||
配列から読み込み
|
||||
|
||||
@param data データ
|
||||
@return ModData
|
||||
"""
|
||||
func read_data( data:PoolByteArray ) -> ModParseResult:
|
||||
var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
|
||||
stream.set_data_array( data )
|
||||
stream.big_endian = true
|
||||
|
||||
var result: = ModParseResult.new( )
|
||||
result.data = self._read( stream )
|
||||
return result
|
||||
|
||||
"""
|
||||
ストリームから読み込み
|
||||
|
||||
@param stream ストリーム
|
||||
@return 読んだ Modファイルデータ
|
||||
"""
|
||||
func _read( stream:StreamPeerBuffer ) -> ModData:
|
||||
var mod: = ModData.new( )
|
||||
mod.module_name = self._read_string( stream, 20 )
|
||||
mod.tracker_name = ""
|
||||
mod.instruments = self._read_instruments( stream )
|
||||
mod.song_length = stream.get_u8( )
|
||||
mod.unknown_number = stream.get_u8( )
|
||||
mod.song_positions = stream.get_partial_data( 128 )[1]
|
||||
var max_song_position:int = 0
|
||||
for sp in mod.song_positions:
|
||||
if max_song_position < sp:
|
||||
max_song_position = sp
|
||||
|
||||
mod.magic = self._read_string( stream, 4 )
|
||||
var channel_count:int = 4
|
||||
match mod.magic:
|
||||
"6CHN":
|
||||
channel_count = 6
|
||||
"FLT8", "8CHN", "CD81", "OKTA":
|
||||
channel_count = 8
|
||||
"16CN":
|
||||
channel_count = 16
|
||||
"32CN":
|
||||
channel_count = 32
|
||||
_:
|
||||
if mod.magic.substr( 2 ) == "CH":
|
||||
channel_count = int( mod.magic )
|
||||
# print( "Unknown magic" )
|
||||
# breakpoint
|
||||
pass
|
||||
mod.channel_count = channel_count
|
||||
|
||||
mod.patterns = self._read_patterns( stream, max_song_position + 1, channel_count )
|
||||
self._read_sample_data( stream, mod.instruments )
|
||||
|
||||
return mod
|
||||
|
||||
"""
|
||||
楽器データを読み込む
|
||||
|
||||
@param stream ストリーム
|
||||
@return 楽器データ
|
||||
"""
|
||||
func _read_instruments( stream:StreamPeerBuffer ) -> Array: # of ModSample
|
||||
var instruments:Array = []
|
||||
|
||||
for i in range( 31 ):
|
||||
var sample: = ModSample.new( )
|
||||
sample.name = self._read_string( stream, 22 )
|
||||
sample.length = stream.get_u16( ) * 2
|
||||
var finetune:int = stream.get_u8( ) & 0x0F
|
||||
if 0x08 < finetune:
|
||||
finetune = finetune - 0x10
|
||||
sample.finetune = finetune * 16
|
||||
sample.volume = stream.get_u8( )
|
||||
sample.loop_start = stream.get_u16( ) * 2
|
||||
sample.loop_length = stream.get_u16( ) * 2
|
||||
if sample.loop_length < 8:
|
||||
sample.loop_type = ModLoopType.NO_LOOP
|
||||
else:
|
||||
sample.loop_type = ModLoopType.FORWARD_LOOP
|
||||
|
||||
var instrument: = ModInstrument.new( )
|
||||
instrument.name = sample.name
|
||||
instrument.samples = []
|
||||
for k in range( 96 ):
|
||||
instrument.samples.append( sample )
|
||||
instrument.volume_envelope = ModEnvelope.new( )
|
||||
instrument.volume_envelope.enabled = false
|
||||
instrument.panning_envelope = ModEnvelope.new( )
|
||||
instrument.panning_envelope.enabled = false
|
||||
|
||||
instruments.append( instrument )
|
||||
|
||||
return instruments
|
||||
|
||||
"""
|
||||
パターンを読み込む
|
||||
|
||||
@param stream ストリームデータ
|
||||
@param max_positions 最大パターン数
|
||||
@param channels 最大チャンネル数
|
||||
@return パターンデータ
|
||||
"""
|
||||
func _read_patterns( stream:StreamPeerBuffer, max_position:int, channels:int ) -> Array:
|
||||
var patterns:Array = []
|
||||
|
||||
for position in range( 0, max_position ):
|
||||
var pattern:Array = []
|
||||
for i in range( 0, 64 ):
|
||||
var line:Array = []
|
||||
for ch in range( 0, channels ):
|
||||
var v1:int = stream.get_u16( )
|
||||
var v2:int = stream.get_u16( )
|
||||
var mod_pattern_note: = ModPatternNote.new( )
|
||||
mod_pattern_note.instrument = ( ( v1 >> 8 ) & 0xF0 ) | ( ( v2 >> 12 ) & 0x0F )
|
||||
mod_pattern_note.key_number = v1 & 0x0FFF
|
||||
mod_pattern_note.effect_command = ( v2 >> 8 ) & 0x0F
|
||||
mod_pattern_note.effect_param = v2 & 0x0FF
|
||||
# TODO: 遅かったら二分探索にでもしませんか
|
||||
if 0 < mod_pattern_note.key_number:
|
||||
mod_pattern_note.note = 23
|
||||
for k in period_table:
|
||||
mod_pattern_note.note += 1
|
||||
if k <= mod_pattern_note.key_number:
|
||||
break
|
||||
else:
|
||||
mod_pattern_note.note = 0
|
||||
line.append( mod_pattern_note )
|
||||
pattern.append( line )
|
||||
patterns.append( pattern )
|
||||
|
||||
return patterns
|
||||
|
||||
"""
|
||||
楽器のデータに基づき、波形を読み込む
|
||||
|
||||
@param stream ストリーム
|
||||
@param instruments 波形を読み込む楽器データリスト
|
||||
"""
|
||||
func _read_sample_data( stream:StreamPeerBuffer, instruments:Array ) -> void:
|
||||
for instrument in instruments:
|
||||
var sample:Mod.ModSample = instrument.samples[0]
|
||||
if 0 < sample.length:
|
||||
sample.data = stream.get_partial_data( sample.length )[1]
|
||||
|
||||
"""
|
||||
文字列の読み込み
|
||||
|
||||
@param stream ストリーム
|
||||
@param size 文字列サイズ
|
||||
@return 読み込んだ文字列を返す
|
||||
"""
|
||||
func _read_string( stream:StreamPeerBuffer, size:int ) -> String:
|
||||
return stream.get_partial_data( size )[1].get_string_from_ascii( )
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/modplayer/ModPlayer.gd" type="Script" id=1]
|
||||
|
||||
[node name="ModPlayer" type="Node"]
|
||||
script = ExtResource( 1 )
|
|
@ -0,0 +1,287 @@
|
|||
"""
|
||||
XM reader by あるる(きのもと 結衣) @arlez80
|
||||
|
||||
This script required Mod.gd for declaring the data structures.
|
||||
|
||||
MIT License
|
||||
"""
|
||||
|
||||
class_name XM
|
||||
|
||||
enum XMNoteFlags {
|
||||
NOTE = 0x01,
|
||||
INSTRUMENT = 0x02,
|
||||
VOLUME = 0x04,
|
||||
EFFECT_COMMAND = 0x08,
|
||||
EFFECT_PARAM = 0x10,
|
||||
|
||||
FIRST_BYTE_AS_FLAGS = 0x80,
|
||||
}
|
||||
|
||||
"""
|
||||
ファイルから読み込む
|
||||
|
||||
@param path ファイルパス
|
||||
@return xm
|
||||
"""
|
||||
func read_file( path:String ) -> Mod.ModParseResult:
|
||||
var f:File = File.new( )
|
||||
var result: = Mod.ModParseResult.new( )
|
||||
|
||||
var err:int = f.open( path, f.READ )
|
||||
if err != OK:
|
||||
result.error = err
|
||||
return result
|
||||
|
||||
var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
|
||||
stream.set_data_array( f.get_buffer( f.get_len( ) ) )
|
||||
f.close( )
|
||||
|
||||
result.data = self._read( stream )
|
||||
if result.data == null:
|
||||
result.error = ERR_PARSE_ERROR
|
||||
return result
|
||||
|
||||
"""
|
||||
配列から読み込む
|
||||
|
||||
@param data データ
|
||||
@return xm
|
||||
"""
|
||||
func read_data( data:PoolByteArray ) -> Mod.ModParseResult:
|
||||
var stream:StreamPeerBuffer = StreamPeerBuffer.new( )
|
||||
stream.set_data_array( data )
|
||||
|
||||
var result: = Mod.ModParseResult.new( )
|
||||
result.data = self._read( stream )
|
||||
if result.data == null:
|
||||
result.error = ERR_PARSE_ERROR
|
||||
return result
|
||||
|
||||
"""
|
||||
ストリームから読み込み
|
||||
|
||||
@param stream ストリーム
|
||||
@return xm
|
||||
"""
|
||||
func _read( stream:StreamPeerBuffer ) -> Mod.ModData:
|
||||
var xm: = Mod.ModData.new( )
|
||||
|
||||
if self._read_string( stream, 17 ) != "Extended Module: ":
|
||||
return null
|
||||
xm.module_name = self._read_string( stream, 20 )
|
||||
if stream.get_u8( ) != 0x1A:
|
||||
return null
|
||||
xm.tracker_name = self._read_string( stream, 20 )
|
||||
xm.version = stream.get_u16( )
|
||||
|
||||
var header_size:int = stream.get_u32( )
|
||||
xm.song_length = stream.get_u16( )
|
||||
xm.restart_position = stream.get_u16( )
|
||||
xm.channel_count = stream.get_u16( )
|
||||
|
||||
var pattern_count:int = stream.get_u16( )
|
||||
var instrument_count:int = stream.get_u16( )
|
||||
xm.flags = stream.get_u16( )
|
||||
xm.init_tick = stream.get_u16( )
|
||||
xm.init_bpm = stream.get_u16( )
|
||||
|
||||
xm.song_positions = stream.get_partial_data( header_size - 20 )[1]
|
||||
|
||||
xm.patterns = self._read_patterns( stream, xm.flags, pattern_count, xm.channel_count )
|
||||
if len( xm.patterns ) == 0:
|
||||
return null
|
||||
|
||||
xm.instruments = self._read_instruments( stream, instrument_count )
|
||||
if len( xm.instruments ) == 0:
|
||||
return null
|
||||
|
||||
return xm
|
||||
|
||||
"""
|
||||
パターン読み込み
|
||||
|
||||
@param stream ストリーム
|
||||
@param xm_file_flags XMデータに関するフラグ
|
||||
@param pattern_count パターン数
|
||||
@param channles 最大チャンネル数
|
||||
@return パターンデータ
|
||||
"""
|
||||
func _read_patterns( stream:StreamPeerBuffer, xm_file_flags:int, pattern_count:int, channels:int ) -> Array:
|
||||
var patterns:Array = []
|
||||
|
||||
for i in range( pattern_count ):
|
||||
var pattern:Array = []
|
||||
|
||||
if stream.get_u32( ) != 9:
|
||||
return []
|
||||
if stream.get_u8( ) != 0:
|
||||
return []
|
||||
var row_count:int = stream.get_u16( )
|
||||
var size:int = stream.get_u16( )
|
||||
|
||||
for k in range( row_count ):
|
||||
var line:Array = []
|
||||
for ch in range( channels ):
|
||||
var patr: = Mod.ModPatternNote.new( )
|
||||
var first:int = stream.get_u8( )
|
||||
if first & XMNoteFlags.FIRST_BYTE_AS_FLAGS != 0:
|
||||
if first & XMNoteFlags.NOTE != 0:
|
||||
patr.note = stream.get_u8( )
|
||||
else:
|
||||
patr.note = first
|
||||
first = 0xFF
|
||||
if 0 < patr.note:
|
||||
patr.note -= 1
|
||||
if first & XMNoteFlags.INSTRUMENT != 0:
|
||||
patr.instrument = stream.get_u8( )
|
||||
if first & XMNoteFlags.VOLUME != 0:
|
||||
patr.volume = stream.get_u8( )
|
||||
if first & XMNoteFlags.EFFECT_COMMAND != 0:
|
||||
patr.effect_command = stream.get_u8( )
|
||||
if first & XMNoteFlags.EFFECT_PARAM != 0:
|
||||
patr.effect_param = stream.get_u8( )
|
||||
if 0 < patr.note:
|
||||
if xm_file_flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0:
|
||||
patr.key_number = self._conv_linear_freq( patr.note )
|
||||
else:
|
||||
patr.key_number = self._conv_amiga_freq( patr.note )
|
||||
line.append( patr )
|
||||
pattern.append( line )
|
||||
patterns.append( pattern )
|
||||
|
||||
return patterns
|
||||
|
||||
"""
|
||||
ノート番号からAMIGA式で周波数計算する
|
||||
|
||||
@param note ノート番号
|
||||
@return 周波数
|
||||
"""
|
||||
func _conv_amiga_freq( note:int ) -> int:
|
||||
return int( 6848.0 / pow( 2.0, note / 12.0 ) )
|
||||
|
||||
"""
|
||||
ノート番号から線形で周波数計算する
|
||||
|
||||
@param note ノート番号
|
||||
@return 周波数
|
||||
"""
|
||||
func _conv_linear_freq( note:int ) -> int:
|
||||
return 7680 - note * 64
|
||||
|
||||
"""
|
||||
楽器データを読み込む
|
||||
|
||||
@param stream ストリーム
|
||||
@param instrument_count 楽器数
|
||||
@return 楽器データ
|
||||
"""
|
||||
func _read_instruments( stream:StreamPeerBuffer, instrument_count:int ) -> Array:
|
||||
var instruments:Array = []
|
||||
|
||||
for i in range( instrument_count ):
|
||||
var inst: = Mod.ModInstrument.new( )
|
||||
var size:int = stream.get_u32( )
|
||||
var remain_size:int = size
|
||||
inst.name = self._read_string( stream, 22 )
|
||||
#print( "%d/%d %s" % [ i, instrument_count, inst.name] )
|
||||
if stream.get_u8( ) != 0:
|
||||
return []
|
||||
|
||||
var sample_count:int = stream.get_u16( )
|
||||
var sample_numbers:Array = []
|
||||
remain_size -= 4 + 22 + 1 + 2
|
||||
if 0 < sample_count:
|
||||
var sample_header_size:int = stream.get_u32( )
|
||||
sample_numbers = stream.get_partial_data( 96 )[1]
|
||||
inst.volume_envelope = Mod.ModEnvelope.new( )
|
||||
for k in range( 12 ):
|
||||
var ve: = Mod.ModEnvelopePoint.new( )
|
||||
ve.frame = stream.get_u16( )
|
||||
ve.value = stream.get_u16( )
|
||||
inst.volume_envelope.points.append( ve )
|
||||
inst.panning_envelope = Mod.ModEnvelope.new( )
|
||||
for k in range( 12 ):
|
||||
var pe: = Mod.ModEnvelopePoint.new( )
|
||||
pe.frame = stream.get_u16( )
|
||||
pe.value = stream.get_u16( )
|
||||
inst.panning_envelope.points.append( pe )
|
||||
remain_size -= 4 + 96 + 48 + 48
|
||||
inst.volume_envelope.point_count = stream.get_u8( )
|
||||
inst.panning_envelope.point_count = stream.get_u8( )
|
||||
inst.volume_envelope.sustain_point = stream.get_u8( )
|
||||
inst.volume_envelope.loop_start_point = stream.get_u8( )
|
||||
inst.volume_envelope.loop_end_point = stream.get_u8( )
|
||||
inst.panning_envelope.sustain_point = stream.get_u8( )
|
||||
inst.panning_envelope.loop_start_point = stream.get_u8( )
|
||||
inst.panning_envelope.loop_end_point = stream.get_u8( )
|
||||
inst.volume_envelope.set_flag( stream.get_u8( ) )
|
||||
inst.panning_envelope.set_flag( stream.get_u8( ) )
|
||||
inst.vibrato_type = stream.get_u8( )
|
||||
inst.vibrato_speed = stream.get_u8( )
|
||||
inst.vibrato_depth = stream.get_u8( )
|
||||
inst.vibrato_depth_shift = stream.get_u8( )
|
||||
inst.volume_fadeout = stream.get_u16( )
|
||||
remain_size -= 16
|
||||
|
||||
if 0 < remain_size:
|
||||
stream.get_partial_data( remain_size ) # reserved
|
||||
|
||||
if 0 < sample_count:
|
||||
var sounds:Array = []
|
||||
# Sound Header
|
||||
for k in range( sample_count ):
|
||||
var xms: = Mod.ModSample.new( )
|
||||
xms.length = stream.get_u32( )
|
||||
xms.loop_start = stream.get_u32( )
|
||||
xms.loop_length = stream.get_u32( )
|
||||
xms.volume = stream.get_u8( )
|
||||
xms.finetune = stream.get_8( )
|
||||
xms.loop_type = stream.get_u8( )
|
||||
if xms.loop_type & 16 != 0:
|
||||
xms.bit = 16
|
||||
xms.loop_type &= 3
|
||||
xms.panning = stream.get_u8( )
|
||||
xms.relative_note = stream.get_8( )
|
||||
stream.get_u8( ) # Reserved
|
||||
xms.name = self._read_string( stream, 22 )
|
||||
sounds.append( xms )
|
||||
# Sound Data
|
||||
for xms in sounds:
|
||||
var d:Array = []
|
||||
var p:int = 0
|
||||
|
||||
if xms.bit == 16:
|
||||
for k in range( xms.length >> 1 ):
|
||||
p += stream.get_16( )
|
||||
d.append( p & 0xFF )
|
||||
d.append( ( p >> 8 ) & 0xFF )
|
||||
else:
|
||||
for k in range( xms.length ):
|
||||
p += stream.get_8( )
|
||||
d.append( p & 0xFF )
|
||||
|
||||
xms.data = PoolByteArray( d )
|
||||
|
||||
inst.samples = []
|
||||
for k in sample_numbers:
|
||||
if len( sounds ) <= k:
|
||||
inst.samples.append( sounds[0] )
|
||||
else:
|
||||
inst.samples.append( sounds[k] )
|
||||
|
||||
instruments.append( inst )
|
||||
|
||||
return instruments
|
||||
|
||||
"""
|
||||
文字列の読み込み
|
||||
|
||||
@param stream ストリーム
|
||||
@param size 文字列サイズ
|
||||
@return 読み込んだ文字列を返す
|
||||
"""
|
||||
func _read_string( stream:StreamPeerBuffer, size:int ) -> String:
|
||||
return stream.get_partial_data( size )[1].get_string_from_ascii( )
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 924 B |
|
@ -0,0 +1,35 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/icon.png-517e88a2da8173eba7cfa3edf0828c77.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modplayer/icon.png"
|
||||
dest_files=[ "res://.import/icon.png-517e88a2da8173eba7cfa3edf0828c77.stex" ]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_mode=0
|
||||
compress/bptc_ldr=0
|
||||
compress/normal_map=0
|
||||
flags/repeat=0
|
||||
flags/filter=false
|
||||
flags/mipmaps=false
|
||||
flags/anisotropic=false
|
||||
flags/srgb=2
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/HDR_as_SRGB=false
|
||||
process/invert_color=false
|
||||
process/normal_map_invert_y=false
|
||||
stream=false
|
||||
size_limit=0
|
||||
detect_3d=false
|
||||
svg/scale=1.0
|
|
@ -0,0 +1,6 @@
|
|||
[plugin]
|
||||
name="Godot Mod Player"
|
||||
description="A plugin that playing Mod Files for Godot Engine."
|
||||
author="arlez80"
|
||||
version="1.4.4"
|
||||
script="GMP.gd"
|
Binary file not shown.
|
@ -0,0 +1,73 @@
|
|||
[gd_scene load_steps=7 format=2]
|
||||
|
||||
[ext_resource path="res://icon.png" type="Texture" id=1]
|
||||
[ext_resource path="res://addons/platformer_controller/platformer_controller.gd" type="Script" id=2]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=1]
|
||||
extents = Vector2( 512, 56 )
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=2]
|
||||
extents = Vector2( 208, 56 )
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=3]
|
||||
extents = Vector2( 32, 304 )
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=4]
|
||||
extents = Vector2( 32, 32 )
|
||||
|
||||
[node name="Main" type="Node2D"]
|
||||
|
||||
[node name="StaticBody2D" type="StaticBody2D" parent="."]
|
||||
position = Vector2( 520, 544 )
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="StaticBody2D"]
|
||||
margin_left = -520.0
|
||||
margin_top = -56.0
|
||||
margin_right = 504.0
|
||||
margin_bottom = 56.0
|
||||
color = Color( 0, 0.443137, 0.027451, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D"]
|
||||
position = Vector2( -8, 0 )
|
||||
shape = SubResource( 1 )
|
||||
|
||||
[node name="StaticBody2D2" type="StaticBody2D" parent="."]
|
||||
position = Vector2( 520, 360 )
|
||||
|
||||
[node name="ColorRect" type="ColorRect" parent="StaticBody2D2"]
|
||||
margin_left = -520.0
|
||||
margin_top = 16.0
|
||||
margin_right = -104.0
|
||||
margin_bottom = 128.0
|
||||
color = Color( 0, 0.443137, 0.027451, 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D2"]
|
||||
position = Vector2( -312, 72 )
|
||||
shape = SubResource( 2 )
|
||||
|
||||
[node name="StaticBody2D3" type="StaticBody2D" parent="."]
|
||||
position = Vector2( 520, 360 )
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="StaticBody2D3"]
|
||||
position = Vector2( -552, -64 )
|
||||
shape = SubResource( 3 )
|
||||
|
||||
[node name="CollisionShape2D2" type="CollisionShape2D" parent="StaticBody2D3"]
|
||||
position = Vector2( 536, -64 )
|
||||
shape = SubResource( 3 )
|
||||
|
||||
[node name="PlatformerController2D" type="KinematicBody2D" parent="."]
|
||||
position = Vector2( 272, 167 )
|
||||
script = ExtResource( 2 )
|
||||
|
||||
[node name="icon" type="Sprite" parent="PlatformerController2D"]
|
||||
texture = ExtResource( 1 )
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="PlatformerController2D"]
|
||||
shape = SubResource( 4 )
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Ev01
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,75 @@
|
|||
# PlatformerController2D
|
||||
|
||||
A 2D platformer class for godot.
|
||||
|
||||
## Changelog
|
||||
### Version 1.0.1
|
||||
- Updated to Godot 3.4
|
||||
- Fixed division by zero error when changing min jump height
|
||||
- Other minor fixes
|
||||
|
||||
## Installation
|
||||
|
||||
1. Add platformer_controller.gd to your project
|
||||
2. Type `extends PlatformerController2D` to the top of your script
|
||||
3. Add these input mappings in your project settings (or you can change the input variables in the inspector)
|
||||
- "move_left"
|
||||
- "move_right"
|
||||
- "jump"
|
||||
|
||||
|
||||
## Features
|
||||
- Double jump
|
||||
- Coyote time
|
||||
- Jump buffer
|
||||
- Hold jump to go higher
|
||||
- Defining jump height and duration (as opposed to setting gravity and jump velocity)
|
||||
- Assymetrical jumps (falling faster than rising)
|
||||
|
||||
<img src="https://github.com/Ev01/PlatformerController2D/raw/assets/jumping.GIF" width="500">| <img src="https://github.com/Ev01/PlatformerController2D/raw/assets/jump_duration.GIF" width="500"> | <img src="https://github.com/Ev01/PlatformerController2D/raw/assets/jump_height.GIF" width="500">
|
||||
--|--|--
|
||||
|
||||
|
||||
|
||||
## Customization / Export variables
|
||||
Here are the values that you can change in the inspector:
|
||||
|
||||
### max_jump_height
|
||||
The max jump height in pixels. You reach this when you hold down jump.
|
||||
|
||||
### Min Jump Height
|
||||
The minimum jump height (tapping jump).
|
||||
|
||||
### Double Jump Height
|
||||
The height of your jump in the air (i.e. double jump, triple jump etc.).
|
||||
|
||||
### Jump Duration
|
||||
How long it takes to get to the peak of the jump (in seconds).
|
||||
|
||||
### Falling Gravity Multiplier
|
||||
Multiplies the gravity by this while falling.
|
||||
|
||||
### Max Jump Amount
|
||||
How many times you can jump before hitting the ground. Set this to 2 for a double jump.
|
||||
|
||||
### Max Acceleration
|
||||
How much you accelerate when you hold left or right (in pixels/sec^2).
|
||||
|
||||
### Friction
|
||||
The higher this number, the more friction is on your character.
|
||||
|
||||
### Can Hold Jump
|
||||
If this is off, you have to press jump down every time you land. If its on you can keep it held.
|
||||
|
||||
### Coyote Time
|
||||
You can still jump this many seconds after falling off a ledge.
|
||||
|
||||
### Jump Buffer
|
||||
Pressing jump this many seconds before hitting the ground will still make you jump.\
|
||||
Note: This is only needed when can_hold_jump is off.
|
||||
|
||||
### Input Variables
|
||||
`input_left`\
|
||||
`input_right`\
|
||||
`input_jump`\
|
||||
Set these to the names of your actions in the Input Map
|
|
@ -0,0 +1,213 @@
|
|||
extends KinematicBody2D
|
||||
|
||||
class_name PlatformerController2D
|
||||
|
||||
# Set these to the name of your action (in the Input Map)
|
||||
export var input_left : String = "move_left"
|
||||
export var input_right : String = "move_right"
|
||||
export var input_jump : String = "jump"
|
||||
|
||||
# The max jump height in pixels (holding jump)
|
||||
export var max_jump_height = 150 setget set_max_jump_height
|
||||
# The minimum jump height (tapping jump)
|
||||
export var min_jump_height = 40 setget set_min_jump_height
|
||||
# The height of your jump in the air
|
||||
export var double_jump_height = 100 setget set_double_jump_height
|
||||
# How long it takes to get to the peak of the jump in seconds
|
||||
export var jump_duration = 0.3 setget set_jump_duration
|
||||
# Multiplies the gravity by this while falling
|
||||
export var falling_gravity_multiplier = 1.5
|
||||
# Set to 2 for double jump
|
||||
export var max_jump_amount = 1
|
||||
export var max_acceleration = 4000
|
||||
export var friction = 8
|
||||
export var can_hold_jump : bool = false
|
||||
# You can still jump this many seconds after falling off a ledge
|
||||
export var coyote_time : float = 0.1
|
||||
# Only neccessary when can_hold_jump is off
|
||||
# Pressing jump this many seconds before hitting the ground will still make you jump
|
||||
export var jump_buffer : float = 0.1
|
||||
|
||||
|
||||
# not used
|
||||
var max_speed = 100
|
||||
var acceleration_time = 10
|
||||
|
||||
|
||||
# These will be calcualted automatically
|
||||
var default_gravity : float
|
||||
var jump_velocity : float
|
||||
var double_jump_velocity : float
|
||||
# Multiplies the gravity by this when we release jump
|
||||
var release_gravity_multiplier : float
|
||||
|
||||
|
||||
var jumps_left : int
|
||||
var holding_jump := false
|
||||
|
||||
var vel = Vector2()
|
||||
var acc = Vector2()
|
||||
|
||||
onready var coyote_timer = Timer.new()
|
||||
onready var jump_buffer_timer = Timer.new()
|
||||
|
||||
|
||||
func _init():
|
||||
default_gravity = calculate_gravity(max_jump_height, jump_duration)
|
||||
jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration)
|
||||
double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
|
||||
release_gravity_multiplier = calculate_release_gravity_multiplier(
|
||||
jump_velocity, min_jump_height, default_gravity)
|
||||
|
||||
|
||||
func _ready():
|
||||
add_child(coyote_timer)
|
||||
coyote_timer.wait_time = coyote_time
|
||||
coyote_timer.one_shot = true
|
||||
|
||||
add_child(jump_buffer_timer)
|
||||
jump_buffer_timer.wait_time = jump_buffer
|
||||
jump_buffer_timer.one_shot = true
|
||||
|
||||
|
||||
func _physics_process(delta):
|
||||
acc.x = 0
|
||||
|
||||
if is_on_floor():
|
||||
coyote_timer.start()
|
||||
if not coyote_timer.is_stopped():
|
||||
jumps_left = max_jump_amount
|
||||
|
||||
if Input.is_action_pressed(input_left):
|
||||
acc.x = -max_acceleration
|
||||
if Input.is_action_pressed(input_right):
|
||||
acc.x = max_acceleration
|
||||
|
||||
|
||||
# Check for ground jumps when we can hold jump
|
||||
if can_hold_jump:
|
||||
if Input.is_action_pressed(input_jump):
|
||||
# Dont use double jump when holding down
|
||||
if is_on_floor():
|
||||
jump()
|
||||
|
||||
# Check for ground jumps when we cannot hold jump
|
||||
if not can_hold_jump:
|
||||
if not jump_buffer_timer.is_stopped() and is_on_floor():
|
||||
jump()
|
||||
|
||||
# Check for jumps in the air
|
||||
if Input.is_action_just_pressed(input_jump):
|
||||
holding_jump = true
|
||||
jump_buffer_timer.start()
|
||||
|
||||
# Only jump in the air when press the button down, code above already jumps when we are grounded
|
||||
if not is_on_floor():
|
||||
jump()
|
||||
|
||||
|
||||
if Input.is_action_just_released(input_jump):
|
||||
holding_jump = false
|
||||
|
||||
|
||||
var gravity = default_gravity
|
||||
|
||||
if vel.y > 0: # If we are falling
|
||||
gravity *= falling_gravity_multiplier
|
||||
|
||||
if not holding_jump and vel.y < 0: # if we released jump and are still rising
|
||||
if not jumps_left < max_jump_amount - 1: # Always jump to max height when we are using a double jump
|
||||
gravity *= release_gravity_multiplier # multiply the gravity so we have a lower jump
|
||||
|
||||
acc.y = -gravity
|
||||
vel.x *= 1 / (1 + (delta * friction))
|
||||
|
||||
vel += acc * delta
|
||||
vel = move_and_slide(vel, Vector2.UP)
|
||||
|
||||
|
||||
|
||||
func calculate_gravity(p_max_jump_height, p_jump_duration):
|
||||
# Calculates the desired gravity by looking at our jump height and jump duration
|
||||
# Formula is from this video https://www.youtube.com/watch?v=hG9SzQxaCm8
|
||||
return (-2 *p_max_jump_height) / pow(p_jump_duration, 2)
|
||||
|
||||
|
||||
func calculate_jump_velocity(p_max_jump_height, p_jump_duration):
|
||||
# Calculates the desired jump velocity by lookihg at our jump height and jump duration
|
||||
return (2 * p_max_jump_height) / (p_jump_duration)
|
||||
|
||||
|
||||
func calculate_jump_velocity2(p_max_jump_height, p_gravity):
|
||||
# Calculates jump velocity from jump height and gravity
|
||||
# formula from
|
||||
# https://sciencing.com/acceleration-velocity-distance-7779124.html#:~:text=in%20every%20step.-,Starting%20from%3A,-v%5E2%3Du
|
||||
return sqrt(-2 * p_gravity * p_max_jump_height)
|
||||
|
||||
|
||||
func calculate_release_gravity_multiplier(p_jump_velocity, p_min_jump_height, p_gravity):
|
||||
# Calculates the gravity when the key is released based on the minimum jump height and jump velocity
|
||||
# Formula is from this website https://sciencing.com/acceleration-velocity-distance-7779124.html
|
||||
var release_gravity = 0 - pow(p_jump_velocity, 2) / (2 * p_min_jump_height)
|
||||
return release_gravity / p_gravity
|
||||
|
||||
|
||||
func calculate_friction(time_to_max):
|
||||
# Formula from https://www.reddit.com/r/gamedev/comments/bdbery/comment/ekxw9g4/?utm_source=share&utm_medium=web2x&context=3
|
||||
# this friction will hit 90% of max speed after the accel time
|
||||
return 1 - (2.30259 / time_to_max)
|
||||
|
||||
|
||||
func calculate_speed(p_max_speed, p_friction):
|
||||
# Formula from https://www.reddit.com/r/gamedev/comments/bdbery/comment/ekxw9g4/?utm_source=share&utm_medium=web2x&context=3
|
||||
return (p_max_speed / p_friction) - p_max_speed
|
||||
|
||||
|
||||
func jump():
|
||||
if jumps_left == max_jump_amount and coyote_timer.is_stopped():
|
||||
# Your first jump must be used when on the ground
|
||||
# If you fall off the ground and then jump you will be using you second jump
|
||||
jumps_left -= 1
|
||||
|
||||
if jumps_left > 0:
|
||||
if jumps_left < max_jump_amount: # If we are double jumping
|
||||
vel.y = -double_jump_velocity
|
||||
else:
|
||||
vel.y = -jump_velocity
|
||||
jumps_left -= 1
|
||||
|
||||
|
||||
coyote_timer.stop()
|
||||
|
||||
|
||||
func set_max_jump_height(value):
|
||||
max_jump_height = value
|
||||
|
||||
default_gravity = calculate_gravity(max_jump_height, jump_duration)
|
||||
jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration)
|
||||
double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
|
||||
release_gravity_multiplier = calculate_release_gravity_multiplier(
|
||||
jump_velocity, min_jump_height, default_gravity)
|
||||
|
||||
|
||||
func set_jump_duration(value):
|
||||
jump_duration = value
|
||||
|
||||
default_gravity = calculate_gravity(max_jump_height, jump_duration)
|
||||
jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration)
|
||||
double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
|
||||
release_gravity_multiplier = calculate_release_gravity_multiplier(
|
||||
jump_velocity, min_jump_height, default_gravity)
|
||||
|
||||
|
||||
func set_min_jump_height(value):
|
||||
min_jump_height = value
|
||||
release_gravity_multiplier = calculate_release_gravity_multiplier(
|
||||
jump_velocity, min_jump_height, default_gravity)
|
||||
|
||||
|
||||
func set_double_jump_height(value):
|
||||
double_jump_height = value
|
||||
double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity)
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
[gd_scene load_steps=12 format=2]
|
||||
|
||||
[ext_resource path="res://assets/images/Clean-Retro-Lines-Player-Plain.png" type="Texture" id=1]
|
||||
[ext_resource path="res://player/Player.gd" type="Script" id=2]
|
||||
[ext_resource path="res://addons/platformer_controller/platformer_controller.gd" type="Script" id=2]
|
||||
|
||||
[sub_resource type="AtlasTexture" id=5]
|
||||
atlas = ExtResource( 1 )
|
||||
|
@ -11,10 +11,6 @@ region = Rect2( 0, 16, 16, 16 )
|
|||
atlas = ExtResource( 1 )
|
||||
region = Rect2( 16, 16, 16, 16 )
|
||||
|
||||
[sub_resource type="AtlasTexture" id=7]
|
||||
atlas = ExtResource( 1 )
|
||||
region = Rect2( 16, 32, 16, 16 )
|
||||
|
||||
[sub_resource type="AtlasTexture" id=1]
|
||||
atlas = ExtResource( 1 )
|
||||
region = Rect2( 0, 0, 16, 16 )
|
||||
|
@ -31,6 +27,10 @@ region = Rect2( 32, 0, 16, 16 )
|
|||
atlas = ExtResource( 1 )
|
||||
region = Rect2( 48, 0, 16, 16 )
|
||||
|
||||
[sub_resource type="AtlasTexture" id=7]
|
||||
atlas = ExtResource( 1 )
|
||||
region = Rect2( 16, 32, 16, 16 )
|
||||
|
||||
[sub_resource type="SpriteFrames" id=8]
|
||||
animations = [ {
|
||||
"frames": [ SubResource( 5 ), SubResource( 6 ) ],
|
||||
|
@ -38,15 +38,15 @@ animations = [ {
|
|||
"name": "idle",
|
||||
"speed": 2.0
|
||||
}, {
|
||||
"frames": [ SubResource( 7 ) ],
|
||||
"loop": true,
|
||||
"name": "jump",
|
||||
"speed": 5.0
|
||||
}, {
|
||||
"frames": [ SubResource( 1 ), SubResource( 2 ), SubResource( 3 ), SubResource( 4 ) ],
|
||||
"loop": true,
|
||||
"name": "walk",
|
||||
"speed": 5.0
|
||||
}, {
|
||||
"frames": [ SubResource( 7 ) ],
|
||||
"loop": true,
|
||||
"name": "jump",
|
||||
"speed": 5.0
|
||||
} ]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id=9]
|
||||
|
@ -54,10 +54,16 @@ extents = Vector2( 4, 7 )
|
|||
|
||||
[node name="Player" type="KinematicBody2D"]
|
||||
script = ExtResource( 2 )
|
||||
max_jump_height = 50
|
||||
min_jump_height = 25
|
||||
double_jump_height = 65
|
||||
max_jump_amount = 2
|
||||
max_acceleration = 1500
|
||||
|
||||
[node name="AnimatedSprite" type="AnimatedSprite" parent="."]
|
||||
frames = SubResource( 8 )
|
||||
animation = "idle"
|
||||
frame = 1
|
||||
playing = true
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
|
|
|
@ -8,6 +8,34 @@
|
|||
|
||||
config_version=4
|
||||
|
||||
_global_script_classes=[ {
|
||||
"base": "Reference",
|
||||
"class": "Mod",
|
||||
"language": "GDScript",
|
||||
"path": "res://addons/modplayer/Mod.gd"
|
||||
}, {
|
||||
"base": "Node",
|
||||
"class": "ModPlayer",
|
||||
"language": "GDScript",
|
||||
"path": "res://addons/modplayer/ModPlayer.gd"
|
||||
}, {
|
||||
"base": "KinematicBody2D",
|
||||
"class": "PlatformerController2D",
|
||||
"language": "GDScript",
|
||||
"path": "res://addons/platformer_controller/platformer_controller.gd"
|
||||
}, {
|
||||
"base": "Reference",
|
||||
"class": "XM",
|
||||
"language": "GDScript",
|
||||
"path": "res://addons/modplayer/XM.gd"
|
||||
} ]
|
||||
_global_script_class_icons={
|
||||
"Mod": "",
|
||||
"ModPlayer": "res://addons/modplayer/icon.png",
|
||||
"PlatformerController2D": "",
|
||||
"XM": ""
|
||||
}
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Retro Platformer"
|
||||
|
@ -15,6 +43,7 @@ run/main_scene="res://levels/Level01.tscn"
|
|||
boot_splash/image="res://assets/images/WithinLogo.png"
|
||||
boot_splash/fullsize=false
|
||||
boot_splash/use_filter=false
|
||||
boot_splash/bg_color=Color( 0.141176, 0.141176, 0.141176, 1 )
|
||||
config/icon="res://icon.png"
|
||||
|
||||
[autoload]
|
||||
|
|
Loading…
Reference in New Issue