import retro platformer controller and mod player

This commit is contained in:
Cadey Ratio 2022-07-20 07:00:32 -04:00
parent 992bdac15e
commit 40d2372f2b
15 changed files with 2127 additions and 10 deletions

23
addons/modplayer/GMP.gd Normal file
View File

@ -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"

290
addons/modplayer/Mod.gd Normal file
View File

@ -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

View File

@ -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 )

287
addons/modplayer/XM.gd Normal file
View File

@ -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( )

BIN
addons/modplayer/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

View File

@ -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

View File

@ -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.

View File

@ -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 )

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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="."]

View File

@ -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]