Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input buffering #16

Open
nonunknown opened this issue Sep 16, 2020 · 6 comments
Open

Input buffering #16

nonunknown opened this issue Sep 16, 2020 · 6 comments
Labels
feature 💡 New feature proposal topic:input

Comments

@nonunknown
Copy link

nonunknown commented Sep 16, 2020

Describe the problem you are having in your project

  • Implementing a combo system
  • Implement a cheat code system

Describe the feature and how it helps to overcome the problem
Input Buffering is common in various game even nowadays like in dark souls used for combat system, also heavily used in fighting games like street fighter and Mortal Kombat, Cheat codes can be easily implemented using input buffers

Describe how your proposal will work, with code, pseudo-code, mockups, and/or diagrams
Implementing a Inputbuffer class:

//Emitted when a registered command is identified by the buffer
signal on_command_identified()

//Register the ID of the input
void register_input(int value);

// name -> the command name (used for signal when archived
// sequence -> the int array representing input sequence e.g [1,2,2] = Up,Left,Left
// total_time -> time in seconds to do the sequence
void add_command(name:String,sequence:PoolIntArray, total_time:float); 

// need to rely on _process() to check for time between key_presses
void update(float delta)


The class must inherit Resource, this way the user can save the sequences.

Use case example:

extends Player

var input_buffer:InputBuffer = InputBuffer.new()

func _ready()
  input_buffer.add_command("haduken", [1,2,2], 0.5)
  connect(self,"on_identified",input_buffer,"on_command_identified")

func _process(delta):
  if ( Input.is_action_just_pressed("ui_right"):
    input_buffer.register_input(1)
  elif ( Input.is_action_just_pressed("ui_left"):
    input_buffer.register_input(2)
  input_buffer.update(delta)

func on_identified(name:String):
  match name:
    "haduken":
      # haduken execution here

Is there a reason why this should be in Goost and not in Godot?
I like to think goost as a bridge for perfect Godot PRs, first make it work, then PR it to godot!

Solve a already made proposal?
Yes, it solves godotengine/godot-proposals#100

** Observations **
Yeah this can be implemented by the final user via GDScript, but I'm into performance, to archive this I'm proposing this relying on bitsets (direct bit manipulation) and way less memory usage, to check in a case where for example a character has like 30 combos into his moveset, the same for cheats and so on!

Pure c++ implementation can be found here -> https://repl.it/repls/RubberyCruelRectangles

@nonunknown nonunknown added the feature 💡 New feature proposal label Sep 16, 2020
@Xrayez
Copy link
Contributor

Xrayez commented Sep 17, 2020

As long as the implementation proves to be useful, I think this could be added, but I have questions!

I don't particularly understand why the sequence has to be encoded as PoolIntArray, as it could be PoolStringArray of input map actions. The engine code can use StringName datatype so that performance could be on par with comparing indices. How would indices map to actions? That's basically the task of InputMap singleton already.

Have you implemented such a class in GDScript before? For things like this I'd start prototyping in GDScript at least, and show that's it's useful in a general-purpose way, I haven't stumbled upon InputBuffer implementation in any form before (not that I have much experience in this...)

By the way, prototype implementations can be shared on https://github.com/goostengine/goost-examples repository.


signal on_command_identified()

Signals don't have on_ prefix throughout the engine code, I'd also rename this to command_detected.

void register_input(int value);

If we go treat the sequence as String actions, this would have to be renamed to register_action(String name), and it would be in alignment with the existing Input API.

// name -> the command name (used for signal when archived
// sequence -> the int array representing input sequence e.g [1,2,2] = Up,Left,Left
// total_time -> time in seconds to do the sequence
void add_command(name:String,sequence:PoolIntArray, total_time:float);

Again, this could be:

void add_command(name:String, sequence:PoolStringArray, total_time:float); 

// need to rely on _process() to check for time between key_presses
void update(float delta)

I've actually submitted a PR to Godot for retrieving the action press/release (toggle) times: godotengine/godot#36460, but it wasn't met with much enthusiasm from the core developers.


Yeah this can be implemented by the final user via GDScript, but I'm into performance, to archive this I'm proposing this relying on bitsets (direct bit manipulation) and way less memory usage, to check in a case where for example a character has like 30 combos into his moveset, the same for cheats and so on!

I can see how this could be needed if you want to implement replay system (?), I'm not sure whether the buffer would require much memory for that for other more simple use cases.

Also, memory usage vs. performance might be mutually exclusive goals to achieve?


That said, I think it's possible to provide both types of API to work on the level of BitSets as you proposed, and the input actions I'm suggesting (being more intuitive for most users I presume), I wonder whether the underlying implementation could cover it all.

@nonunknown
Copy link
Author

Hmm I didnt tought on using string name of the input actions, its a interesting approach.

@Xrayez
Copy link
Contributor

Xrayez commented Sep 22, 2020

An observation: InputBuffer was implemented as part of godotengine/godot#37200 (not what this proposal describes), but then renamed to DataBuffer it seems, talking about possible naming confusion, so might be worth renaming the class to something more descriptive.

@realkotob
Copy link

@Xrayez I could be mistaken since I haven't dug into that PR a lot but it's probably because it's intended to be general and possibly used for more than just input, such as syncing positions or whatever. (Although even in the general sense these can be considered inputs for the backend as well)

@Xrayez
Copy link
Contributor

Xrayez commented Sep 22, 2020

Yes, to clarify, I'm not suggesting renaming the proposed InputBuffer to DataBuffer, but to something more like InputCommands, InputSequence (to better reflect the fact that it depends on the time rather than pure data) or whatever else. It does make sense for the DataBuffer name in the godotengine/godot#37200.

godotengine/godot#36460 presents something which involves the time factor as well, so maybe the logic could be combined somehow. It would make sense to me that those input sequences could be added and fetched just like input actions in the input map, for which there's InputMap singleton. Perhaps it's worth to implement a similar input mapping singleton, which could also provide all those time-related queries related to actions.

If we go with the singleton approach, I'd add InputSequenceMap singleton which can:

  • register a sequence of key presses which can trigger a particular action.
  • fire a signal when any such input sequence is performed/detected.

That would be core functionality, of course it would be up to you to decide how you can fill out those input sequences, but an editor could also be created for this, in theory.

In the OP:

The class must inherit Resource, this way the user can save the sequences.

Input actions are saved as part of project.godot currently, similar approach could be used to save input sequences to project.godot, but instead of mapping keys to actions, you'd map actions with timelines to "commands".

@nonunknown
Copy link
Author

nonunknown commented Oct 1, 2020

Made A GDScript prototype, any feedbacks are welcome @Xrayez @asheraryam

InputBuffer.gd:

extends Reference
class_name InputBuffer

signal command_identified

var _commands:Array
var _buffer:PoolIntArray = []
var _time_buffer:PoolRealArray = []
var _time:float = 0

func _init(target:Node):
	connect("command_identified",target,"_on_command_idenfied")
	pass

func add_command(_name:String,_sequence:PoolIntArray,total_time:float):
	var error:bool  = false
	for cmd in _commands:
		if cmd.name == _name:
			printerr("Name: %s already exists please change" % _name)
			error = true
			break
	if total_time < 0.1:
		error = true
		printerr("Total Time must be more than 0.99s")
	
	if error: return
	
	var cmd:Command = Command.new()
	cmd.name = _name
	cmd.action_sequence = _sequence
	cmd.total_time = total_time
	
	_commands.append(cmd)
	
	print(_commands)
	
	pass

func register_action(action:int):
	_buffer.append(action)
	if _time_buffer.empty(): 
		_time_buffer.append(0.0)
	else:
		_time_buffer.append(_time)
	_time = 0.0
	prints(action,_buffer,_time_buffer)
	_check_match()
	pass

func update(delta:float):
	_time += delta
	pass

func _clear_all():
	_buffer = []
	_time_buffer = []
	_time = 0.0

func _check_match():
	var identified:String = ""
	var index = _buffer.size()-1
	if _buffer.size() < 3: return
	for cmd in _commands:
		
		var action:PoolIntArray = cmd.action_sequence
		print("spected: %s" % str(action))
		var result:PoolIntArray = []
		var done_in:float = 0.0
		for i in range(action.size()-1,-1,-1):
			result.append(_buffer[(_buffer.size()-1)-i])
			done_in += _time_buffer[(_time_buffer.size()-1)-i ]
		print("got: %s " % str(result))
		print("total time: %d" % done_in)
		if result == action and done_in <= cmd.total_time:
			identified = cmd.name
			_clear_all()
			break
	# Array = [A,B,C,D,E]
	# Array = [0,1,2,3,4]
	
	if not identified.empty():
		emit_signal("command_identified",identified)
	pass

Command.gd:

extends Resource
class_name Command

export var name:String = ""
export var action_sequence:PoolIntArray = []
export var total_time:float = 0.0

Test.gd:

extends Control

enum inputs {UP,DOWN,LEFT,RIGHT}
var input:InputBuffer = InputBuffer.new(self)

func _ready():
	input.add_command("haduken",[inputs.UP,inputs.DOWN,inputs.DOWN],1)
	input.add_command("test",[inputs.DOWN,inputs.LEFT,inputs.LEFT],1)
	pass # Replace with function body.

func _process(delta):
	
	if (Input.is_action_just_pressed("ui_down")): input.register_action(inputs.DOWN)
	elif (Input.is_action_just_pressed("ui_up")): input.register_action(inputs.UP)
	elif (Input.is_action_just_pressed("ui_left")): input.register_action(inputs.LEFT)
	elif (Input.is_action_just_pressed("ui_right")): input.register_action(inputs.RIGHT)
	
	input.update(delta)

func _on_command_idenfied(result:String):
	match result:
		"haduken":
			print("haduken")
		"test":
			print("test")



Goost edit: test project
input_buffer.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 💡 New feature proposal topic:input
Projects
None yet
Development

No branches or pull requests

3 participants