Skip to content

Commit b606d4f

Browse files
author
Roberto De Ioris
committed
new improved event handling system
1 parent 91c3ebe commit b606d4f

13 files changed

Lines changed: 229 additions & 126 deletions

‎Source/UnrealEnginePython/Private/UEPyModule.cpp‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "UEPyEditor.h"
2222
#endif
2323

24+
#include "PythonDelegate.h"
2425

2526

2627
DEFINE_LOG_CATEGORY(LogPython);
@@ -125,6 +126,9 @@ static PyMethodDef ue_PyUObject_methods[] = {
125126
{ "get_owner", (PyCFunction)py_ue_get_owner, METH_VARARGS, "" },
126127
{ "get_name", (PyCFunction)py_ue_get_name, METH_VARARGS, "" },
127128
{ "get_full_name", (PyCFunction)py_ue_get_full_name, METH_VARARGS, "" },
129+
130+
{ "bind_event", (PyCFunction)py_ue_bind_event, METH_VARARGS, "" },
131+
128132
#if WITH_EDITOR
129133
{ "get_actor_label", (PyCFunction)py_ue_get_actor_label, METH_VARARGS, "" },
130134
{ "set_actor_label", (PyCFunction)py_ue_set_actor_label, METH_VARARGS, "" },
@@ -557,6 +561,11 @@ bool ue_py_convert_pyobject(PyObject *py_obj, UProperty *prop, uint8 *buffer) {
557561
return true;
558562
}
559563

564+
// encode a dictionary to a struct
565+
if (PyDict_Check(py_obj)) {
566+
return false;
567+
}
568+
560569
if (PyObject_IsInstance(py_obj, (PyObject *)&ue_PyUObjectType)) {
561570
ue_PyUObject *ue_obj = (ue_PyUObject *)py_obj;
562571
if (ue_obj->ue_object->IsA<UClass>()) {
@@ -586,3 +595,73 @@ ue_PyUObject *ue_is_pyuobject(PyObject *obj) {
586595
return nullptr;
587596
return (ue_PyUObject *)obj;
588597
}
598+
599+
// automatically bind events based on class methods names
600+
void ue_autobind_events_for_class(UObject *u_obj, PyObject *py_class) {
601+
PyObject *attrs = PyObject_Dir(py_class);
602+
if (!attrs)
603+
return;
604+
605+
Py_ssize_t len = PyList_Size(attrs);
606+
for (Py_ssize_t i = 0; i < len; i++) {
607+
PyObject *py_attr_name = PyList_GetItem(attrs, i);
608+
if (!py_attr_name || !PyUnicode_Check(py_attr_name))
609+
continue;
610+
FString attr_name = UTF8_TO_TCHAR(PyUnicode_AsUTF8(py_attr_name));
611+
if (!attr_name.StartsWith("on_", ESearchCase::CaseSensitive))
612+
continue;
613+
// check if the attr is a callable
614+
PyObject *item = PyObject_GetAttrString(py_class, TCHAR_TO_UTF8(*attr_name));
615+
if (item && PyCallable_Check(item)) {
616+
TArray<FString> parts;
617+
if (attr_name.ParseIntoArray(parts, UTF8_TO_TCHAR("_")) > 1) {
618+
FString event_name;
619+
for (FString part : parts) {
620+
FString first_letter = part.Left(1).ToUpper();
621+
part.RemoveAt(0);
622+
event_name = event_name.Append(first_letter);
623+
event_name = event_name.Append(part);
624+
}
625+
// do not fail on wrong properties
626+
ue_bind_event(u_obj, event_name, item, false);
627+
}
628+
}
629+
}
630+
}
631+
632+
PyObject *ue_bind_event(UObject *u_obj, FString event_name, PyObject *py_callable, bool fail_on_wrong_property) {
633+
634+
UProperty *u_property = u_obj->GetClass()->FindPropertyByName(FName(*event_name));
635+
if (!u_property) {
636+
if (fail_on_wrong_property)
637+
return PyErr_Format(PyExc_Exception, "unable to find event property %s", event_name);
638+
Py_INCREF(Py_None);
639+
return Py_None;
640+
}
641+
642+
if (auto casted_prop = Cast<UMulticastDelegateProperty>(u_property)) {
643+
FMulticastScriptDelegate multiscript_delegate = casted_prop->GetPropertyValue_InContainer(u_obj);
644+
645+
FScriptDelegate script_delegate;
646+
UPythonDelegate *py_delegate = NewObject<UPythonDelegate>();
647+
py_delegate->SetPyCallable(py_callable);
648+
py_delegate->SetSignature(casted_prop->SignatureFunction);
649+
650+
// fake UFUNCTION for bypassing checks
651+
script_delegate.BindUFunction(py_delegate, FName("PyFakeCallable"));
652+
653+
// add the new delegate
654+
multiscript_delegate.Add(script_delegate);
655+
656+
// re-assign multicast delegate
657+
casted_prop->SetPropertyValue_InContainer(u_obj, multiscript_delegate);
658+
659+
}
660+
else {
661+
if (fail_on_wrong_property)
662+
return PyErr_Format(PyExc_Exception, "property %s is not an event", event_name);
663+
}
664+
665+
Py_INCREF(Py_None);
666+
return Py_None;
667+
}

‎Source/UnrealEnginePython/Private/UEPyModule.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@ UWorld *ue_get_uworld(ue_PyUObject *);
2626
AActor *ue_get_actor(ue_PyUObject *);
2727
PyObject *ue_py_convert_property(UProperty *, uint8 *);
2828
bool ue_py_convert_pyobject(PyObject *, UProperty *, uint8 *);
29-
ue_PyUObject *ue_is_pyuobject(PyObject *);
29+
ue_PyUObject *ue_is_pyuobject(PyObject *);
30+
31+
void ue_autobind_events_for_class(UObject *, PyObject *);
32+
PyObject *ue_bind_event(UObject *, FString, PyObject *, bool);

‎Source/UnrealEnginePython/Private/UEPyObject.cpp‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "UnrealEnginePythonPrivatePCH.h"
22

3+
#include "PythonDelegate.h"
34

45
PyObject *py_ue_get_class(ue_PyUObject * self, PyObject * args) {
56

@@ -239,3 +240,21 @@ PyObject *py_ue_remove_from_root(ue_PyUObject *self, PyObject * args) {
239240
return Py_None;
240241
}
241242

243+
PyObject *py_ue_bind_event(ue_PyUObject * self, PyObject * args) {
244+
245+
ue_py_check(self);
246+
247+
char *event_name;
248+
PyObject *py_callable;
249+
if (!PyArg_ParseTuple(args, "sO:bind_event", &event_name, &py_callable)) {
250+
return NULL;
251+
}
252+
253+
254+
if (!PyCallable_Check(py_callable)) {
255+
return PyErr_Format(PyExc_Exception, "object is not callable");
256+
}
257+
258+
return ue_bind_event(self->ue_object, FString(event_name), py_callable, true);
259+
}
260+

‎Source/UnrealEnginePython/Private/UEPyObject.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ PyObject *py_ue_call(ue_PyUObject *, PyObject *);
1616
PyObject *py_ue_get_property(ue_PyUObject *, PyObject *);
1717
PyObject *py_ue_is_rooted(ue_PyUObject *, PyObject *);
1818
PyObject *py_ue_add_to_root(ue_PyUObject *, PyObject *);
19-
PyObject *py_ue_remove_from_root(ue_PyUObject *, PyObject *);
19+
PyObject *py_ue_remove_from_root(ue_PyUObject *, PyObject *);
20+
21+
PyObject *py_ue_bind_event(ue_PyUObject *, PyObject *);

‎Source/UnrealEnginePython/Private/UEPyTraceAndSweep.cpp‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ PyObject *py_ue_line_trace_single_by_channel(ue_PyUObject * self, PyObject * arg
2424
bool got_hit = world->LineTraceSingleByChannel(hit, FVector(x1, y1, z1), FVector(x2, y2, z2), (ECollisionChannel)channel);
2525

2626
if (got_hit) {
27-
PyObject *ret = (PyObject *)ue_get_python_wrapper(hit.GetActor());
28-
if (!ret)
29-
return PyErr_Format(PyExc_Exception, "uobject is in invalid state");
30-
return Py_BuildValue("Offffff", ret, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z);
27+
AActor *actor = hit.GetActor();
28+
if (actor) {
29+
PyObject *ret = (PyObject *)ue_get_python_wrapper(actor);
30+
if (!ret)
31+
return PyErr_Format(PyExc_Exception, "uobject is in invalid state");
32+
return Py_BuildValue("Offffff", ret, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z);
33+
}
3134
}
3235

3336
return Py_BuildValue("Offffff", Py_None, 0, 0, 0, 0, 0, 0);
@@ -62,9 +65,12 @@ PyObject *py_ue_line_trace_multi_by_channel(ue_PyUObject * self, PyObject * args
6265
if (got_hits) {
6366
for (int i = 0; i < hits.Num(); i++) {
6467
FHitResult hit = hits[i];
65-
PyObject *ret = (PyObject *)ue_get_python_wrapper(hit.GetActor());
66-
if (ret) {
67-
PyList_Append(hits_list, Py_BuildValue("Offffff", ret, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z));
68+
AActor *actor = hit.GetActor();
69+
if (actor) {
70+
PyObject *ret = (PyObject *)ue_get_python_wrapper(actor);
71+
if (ret) {
72+
PyList_Append(hits_list, Py_BuildValue("Offffff", ret, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z));
73+
}
6874
}
6975
}
7076
}
@@ -105,10 +111,13 @@ PyObject *py_ue_get_hit_result_under_cursor(ue_PyUObject * self, PyObject * args
105111
bool got_hit = controller->GetHitResultUnderCursor((ECollisionChannel)channel, complex, hit);
106112

107113
if (got_hit) {
108-
PyObject *ret = (PyObject *)ue_get_python_wrapper(hit.GetActor());
109-
if (!ret)
110-
return PyErr_Format(PyExc_Exception, "uobject is in invalid state");
111-
return Py_BuildValue("Offffff", ret, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z);
114+
AActor *actor = hit.GetActor();
115+
if (actor) {
116+
PyObject *ret = (PyObject *)ue_get_python_wrapper(actor);
117+
if (!ret)
118+
return PyErr_Format(PyExc_Exception, "uobject is in invalid state");
119+
return Py_BuildValue("Offffff", ret, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z);
120+
}
112121
}
113122

114123
return Py_BuildValue("Offffff", Py_None, 0, 0, 0, 0, 0, 0);

‎Source/UnrealEnginePython/Public/PyActor.cpp‎

Lines changed: 4 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,8 @@ APyActor::APyActor()
77
{
88
PrimaryActorTick.bCanEverTick = true;
99

10-
this->OnActorBeginOverlap.AddDynamic(this, &APyActor::PyOnActorBeginOverlap);
11-
this->OnActorEndOverlap.AddDynamic(this, &APyActor::PyOnActorEndOverlap);
12-
this->OnActorHit.AddDynamic(this, &APyActor::PyOnActorHit);
13-
this->OnClicked.AddDynamic(this, &APyActor::PyOnActorClicked);
14-
1510
PythonTickForceDisabled = false;
11+
PythonDisableAutoBinding = false;
1612

1713
// pre-generate PyUObject (for performance)
1814
ue_get_python_wrapper(this);
@@ -74,6 +70,9 @@ void APyActor::BeginPlay()
7470
SetActorTickEnabled(false);
7571
}
7672

73+
if (!PythonDisableAutoBinding)
74+
ue_autobind_events_for_class(this, py_actor_instance);
75+
7776
if (!PyObject_HasAttrString(py_actor_instance, "begin_play"))
7877
return;
7978

@@ -107,61 +106,6 @@ void APyActor::Tick(float DeltaTime)
107106
}
108107

109108

110-
void APyActor::PyOnActorBeginOverlap(AActor *overlapped, AActor *other)
111-
{
112-
if (!py_actor_instance)
113-
return;
114-
115-
PyObject *ret = PyObject_CallMethod(py_actor_instance, "on_actor_begin_overlap", "O", (PyObject *)ue_get_python_wrapper(other));
116-
if (!ret) {
117-
unreal_engine_py_log_error();
118-
return;
119-
}
120-
Py_DECREF(ret);
121-
}
122-
123-
void APyActor::PyOnActorEndOverlap(AActor *overlapped, AActor *other)
124-
{
125-
if (!py_actor_instance)
126-
return;
127-
128-
if (!PyObject_HasAttrString(py_actor_instance, "on_actor_end_overlap"))
129-
return;
130-
131-
PyObject *ret = PyObject_CallMethod(py_actor_instance, "on_actor_end_overlap", "O", (PyObject *)ue_get_python_wrapper(other));
132-
if (!ret) {
133-
unreal_engine_py_log_error();
134-
return;
135-
}
136-
Py_DECREF(ret);
137-
}
138-
139-
void APyActor::PyOnActorHit(AActor *self_actor, AActor *other, FVector impulse_normal, const FHitResult &hit)
140-
{
141-
if (!py_actor_instance)
142-
return;
143-
144-
PyObject *ret = PyObject_CallMethod(py_actor_instance, "on_actor_hit", "O(fff)(fff)(fff)", (PyObject *)ue_get_python_wrapper(other), impulse_normal.X, impulse_normal.Y, impulse_normal.Z, hit.ImpactPoint.X, hit.ImpactPoint.Y, hit.ImpactPoint.Z, hit.ImpactNormal.X, hit.ImpactNormal.Y, hit.ImpactNormal.Z);
145-
if (!ret) {
146-
unreal_engine_py_log_error();
147-
return;
148-
}
149-
Py_DECREF(ret);
150-
}
151-
152-
void APyActor::PyOnActorClicked(AActor *touched_actor, FKey button_pressed)
153-
{
154-
if (!py_actor_instance)
155-
return;
156-
157-
PyObject *ret = PyObject_CallMethod(py_actor_instance, "on_actor_clicked", "Os", (PyObject *)ue_get_python_wrapper(touched_actor), TCHAR_TO_UTF8(*button_pressed.ToString()));
158-
if (!ret) {
159-
unreal_engine_py_log_error();
160-
return;
161-
}
162-
Py_DECREF(ret);
163-
}
164-
165109
void APyActor::CallPythonActorMethod(FString method_name, FString args)
166110
{
167111
if (!py_actor_instance)

‎Source/UnrealEnginePython/Public/PyActor.h‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class APyActor : public AActor
3232
UPROPERTY(EditAnywhere, Category = "Python")
3333
bool PythonTickForceDisabled;
3434

35+
UPROPERTY(EditAnywhere, Category = "Python")
36+
bool PythonDisableAutoBinding;
37+
3538
UFUNCTION(BlueprintCallable, Category = "Python")
3639
void CallPythonActorMethod(FString method_name, FString args);
3740

@@ -43,17 +46,5 @@ class APyActor : public AActor
4346

4447
private:
4548
PyObject *py_actor_instance;
46-
47-
UFUNCTION()
48-
void PyOnActorBeginOverlap(AActor *overlapped, AActor *other);
49-
50-
UFUNCTION()
51-
void PyOnActorEndOverlap(AActor *overlapped, AActor *other);
52-
53-
UFUNCTION()
54-
void PyOnActorHit(AActor *self_actor, AActor *other, FVector normal_impulse, const FHitResult &hit);
55-
56-
UFUNCTION()
57-
void PyOnActorClicked(AActor *touched_actor, FKey button_pressed);
5849
};
5950

‎Source/UnrealEnginePython/Public/PyPawn.cpp‎

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ APyPawn::APyPawn()
66
{
77
PrimaryActorTick.bCanEverTick = true;
88

9-
this->OnActorBeginOverlap.AddDynamic(this, &APyPawn::PyOnActorBeginOverlap);
10-
this->OnClicked.AddDynamic(this, &APyPawn::PyOnActorClicked);
11-
129
PythonTickForceDisabled = false;
10+
PythonDisableAutoBinding = false;
1311

1412
// pre-generate PyUObject (for performance)
1513
ue_get_python_wrapper(this);
@@ -72,6 +70,9 @@ void APyPawn::BeginPlay()
7270
SetActorTickEnabled(false);
7371
}
7472

73+
if (!PythonDisableAutoBinding)
74+
ue_autobind_events_for_class(this, py_pawn_instance);
75+
7576
if (!PyObject_HasAttrString(py_pawn_instance, "begin_play"))
7677
return;
7778

@@ -102,38 +103,6 @@ void APyPawn::Tick(float DeltaTime)
102103
}
103104

104105

105-
void APyPawn::PyOnActorBeginOverlap(AActor *overlapped, AActor *other)
106-
{
107-
if (!py_pawn_instance)
108-
return;
109-
110-
if (!PyObject_HasAttrString(py_pawn_instance, "on_actor_begin_overlap"))
111-
return;
112-
113-
PyObject *ret = PyObject_CallMethod(py_pawn_instance, "on_actor_begin_overlap", "O", (PyObject *)ue_get_python_wrapper(other));
114-
if (!ret) {
115-
unreal_engine_py_log_error();
116-
return;
117-
}
118-
Py_DECREF(ret);
119-
}
120-
121-
void APyPawn::PyOnActorClicked(AActor *touched_actor, FKey button_pressed)
122-
{
123-
if (!py_pawn_instance)
124-
return;
125-
126-
if (!PyObject_HasAttrString(py_pawn_instance, "on_actor_clicked"))
127-
return;
128-
129-
PyObject *ret = PyObject_CallMethod(py_pawn_instance, "on_actor_clicked", "Os", (PyObject *)ue_get_python_wrapper(touched_actor), TCHAR_TO_UTF8(*button_pressed.ToString()));
130-
if (!ret) {
131-
unreal_engine_py_log_error();
132-
return;
133-
}
134-
Py_DECREF(ret);
135-
}
136-
137106
void APyPawn::CallPythonPawnMethod(FString method_name)
138107
{
139108
if (!py_pawn_instance)

0 commit comments

Comments
 (0)