Compare commits

..

67 Commits

Author SHA1 Message Date
Davy Hélard
62ed7a4998 Put an icon on empty shape painter instances 2025-10-11 15:51:09 +02:00
Davy Hélard
dc5dab7086 Fix particle helper destruction 2025-10-11 15:48:43 +02:00
Davy Hélard
dad7546099 Display the 2D particle emitter renderer 2025-10-09 18:49:56 +02:00
Davy Hélard
0a55eb46d3 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-10-09 17:28:15 +02:00
Davy Hélard
2678d86783 Forbid selection when transform controls are used 2025-10-09 17:03:43 +02:00
Davy Hélard
b31618568a Fix default grid depth. 2025-10-09 16:16:10 +02:00
Davy Hélard
b3b4681c51 Fix grid not following the object 2025-10-09 15:33:42 +02:00
Davy Hélard
b1b7643efc Fix rotation controls 2025-10-09 14:58:07 +02:00
Davy Hélard
d43479cbe5 Avoid too big angles 2025-10-09 14:15:53 +02:00
Clément Pasteau
11d3c52197 Automatically scroll down the AI Panel when generating (#7888) 2025-10-08 15:17:11 +02:00
github-actions[bot]
2ee1050083 [Auto PR] Update translations (#7886) 2025-10-08 13:31:58 +02:00
Clément Pasteau
ca00afb918 Allow coming back from bundle purchase flow & improve display (#7887)
Do not show in changelog
2025-10-08 11:10:05 +02:00
Florian Rival
02953a1436 Fix warning 2025-10-08 09:34:04 +02:00
Florian Rival
e14215c679 Make GDJS runtime of the deployed web-app to use the exact commit hash
This avoids issues when deploying again the same version number

Don't show in changelog
2025-10-08 00:51:34 +02:00
github-actions[bot]
c60ea0701a [Auto PR] Update translations (#7885) 2025-10-07 12:43:15 +02:00
Digvijay Rawat
de6ae1cc8f Save the selected layer in each scene so it is persisted across sessions (#7879) 2025-10-07 12:31:47 +02:00
Clément Pasteau
ad24acd72f Allow buying a bundle without an account (#7873)
Do not show in changelog
2025-10-07 12:30:13 +02:00
Florian Rival
3c0bb83032 Add "Pick Nearest" and "Rotate toward object" action (#7883) 2025-10-07 11:59:07 +02:00
Florian Rival
d5c96d74ed Add a grace distance to Destroy Outside behavior to avoid deleting objects if they are never seen but near the camera (#7864) 2025-10-07 11:58:42 +02:00
github-actions[bot]
43aada8ae7 [Auto PR] Update translations (#7881) 2025-10-07 11:55:22 +02:00
Florian Rival
17a7c2815f Send GDevelop version to AI endpoints for backward compatibility
Don't show in changelog
2025-10-06 18:46:31 +02:00
Sebastien Dionne
9441774a22 Fix typos and linguistic errors in documentation / hacktoberfest (#7882)
Signed-off-by: Sebastien Dionne <survivant00@gmail.com>
2025-10-06 11:44:05 +02:00
Florian Rival
d279276cc0 Fix instance sometimes not visible after being added by the AI 2025-10-05 23:52:35 +02:00
Florian Rival
c7f8a7a2eb Fix formatting 2025-10-05 23:44:53 +02:00
D8H
0fb92e000b Fix the link to the asynchronous function help page (#7880) 2025-10-05 23:27:18 +02:00
Florian Rival
76d43e1695 Fix instances not visible when added by the AI 2025-10-05 22:43:16 +02:00
Florian Rival
d70e3f71a6 Improve description of debugger tools extension [ci skip]
Don't show in changelog
2025-10-04 01:58:13 +02:00
Florian Rival
fe50ea3c01 Improve fog density description 2025-10-03 20:01:52 +02:00
danvervlad
77f821250a Add internal flag allowing to disable entirely hitboxes of Spine objects to avoid unnecessary recomputations (#7834)
Only show in developer changelog
2025-10-03 18:30:37 +02:00
Davy Hélard
c3ee9d9891 Display the grid for duplicated and dragged objects 2025-10-03 17:45:26 +02:00
Davy Hélard
313f7857e2 Use the yellow point instead of Alt to drag the selection 2025-10-03 16:26:10 +02:00
github-actions[bot]
ff028ffa62 [Auto PR] Update translations (#7860)
Co-authored-by: D8H <2611977+D8H@users.noreply.github.com>
2025-10-03 15:29:54 +02:00
Davy Hélard
3ac2aec229 Show the grid when dragging a new instance 2025-10-03 14:11:08 +02:00
Davy Hélard
92e8dfdf04 Keep the camera angle when centering on an object 2025-10-03 10:43:31 +02:00
Davy Hélard
80726da56d Add a getter for areEffectsHiddenInEditor. 2025-10-03 10:39:45 +02:00
Davy Hélard
3098fb034f Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-10-02 14:16:49 +02:00
D8H
64cc788d43 Save grid settings for variant tabs (#7869) 2025-10-02 14:15:41 +02:00
D8H
a85668c8d9 Fix aspect ratio being lost when hot-reloading 3D models (#7868) 2025-10-02 14:15:26 +02:00
Davy Hélard
9d0f172663 Format 2025-10-02 13:25:36 +02:00
Davy Hélard
e0d2f16dc1 Fix aspect ratio being lost when hot-reloading 3D models 2025-10-02 11:31:22 +02:00
Davy Hélard
0578c45b0f Additional rounding of object locations 2025-10-01 16:35:28 +02:00
Davy Hélard
a8866a7bd6 Use the origin point for translation and scaling 2025-10-01 11:49:16 +02:00
Davy Hélard
ec62e33530 Merge remote-tracking branch 'official/master' into move-instance-in-game-2 2025-09-30 18:10:57 +02:00
Davy Hélard
e1f12837f1 Fix hot-reloading not doing anything 2025-09-30 16:28:44 +02:00
Davy Hélard
732f3c14b9 Add a missing variant list in tests 2025-09-30 14:44:01 +02:00
Davy Hélard
7fdad45192 Add missing InstancesEditorSettings in tests 2025-09-30 14:31:46 +02:00
Clément Pasteau
add042ec16 Create a standalone page that can be used for bundles (#7861) 2025-09-30 14:20:53 +02:00
Davy Hélard
94590d3713 Fix to avoid to hot-reload the editor with game and the other way around 2025-09-30 13:04:13 +02:00
Davy Hélard
de310512a3 Fix captures not being done at preview 2025-09-30 13:04:13 +02:00
Florian Rival
a9edd2f21a Ensure Box2D wasm is properly loaded whatever the environment/protocol (#7862)
Tested on: 
- [x] preview (web-app, cloud project)
- [x] in-game editor (web-app, cloud project)
- [x] preview (electron cloud project, electron local project)
- [x] in-game editor (electron cloud project, electron local project)
- [x] cordova android
- [x] cordova ios
- [x] electron export (windows, macos)
2025-09-30 12:16:45 +02:00
Davy Hélard
0cc03bd4b5 Fix hot-reload losing model aspect ratio 2025-09-29 19:25:42 +02:00
Florian Rival
cdbcaf59d9 Fix framerate when dragging a new object 2025-09-29 17:10:36 +02:00
Florian Rival
4e9b09e426 Fix border color with AI pane 2025-09-29 17:08:03 +02:00
Clément Pasteau
e174136fb4 New Save & Load built-in actions (#7811)
* New actions are available to save & load the game state, making Saving & Loading as easy as adding 1 action to your game!
* While it will work in most cases, it has a few limitations and hasn't been thoroughly tested on all types of objects/behaviors and games, so it is for the moment tagged as **Experimental** while we gather feedback and improve it
* Check out the wiki for more info: https://wiki.gdevelop.io/gdevelop5/all-features/save-state
2025-09-29 15:38:37 +02:00
Davy Hélard
3d843a0170 Fix grid opacity 2025-09-29 14:10:24 +02:00
Davy Hélard
ee2f7fb8dc Fix the grid to snap on the object origin 2025-09-29 13:58:34 +02:00
Davy Hélard
a780601230 Fix custom object selection 2025-09-29 11:21:02 +02:00
Florian Rival
cc42923e16 Add support for touch controls in editor 2025-09-28 19:21:06 +02:00
Davy Hélard
0e8a223b24 Fix controls being put at (0; 0; 0) when switching of scene and a custom object is selected 2025-09-28 19:00:21 +02:00
github-actions[bot]
ff8697ed71 [Auto PR] Update translations (#7852) 2025-09-26 10:23:40 +02:00
ViktorVovk
542a841791 Add error handling for Howler sound methods (#7853)
* Specifically for "Maximum call stack size exceeded" errors.
2025-09-26 10:11:58 +02:00
github-actions[bot]
b9640f0049 [Auto PR] Update translations (#7851)
Do not show in changelog
2025-09-25 17:51:05 +02:00
Florian Rival
a6cc3dc85b Update storage action to make clearer the usage of manual load/unload 2025-09-25 16:28:36 +02:00
Clément Pasteau
6083c71e0e Bump newIDE version (#7850) 2025-09-25 10:48:53 +02:00
github-actions[bot]
e844ea819b Update extension translations [skip ci] (#7849)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-25 09:38:37 +02:00
github-actions[bot]
27f0300bc7 Update translations [skip ci] (#7845)
Co-authored-by: ClementPasteau <4895034+ClementPasteau@users.noreply.github.com>
2025-09-25 09:35:00 +02:00
Clément Pasteau
35cea2eaba Update example slug info for analytics (#7847)
Do not show in changelog
2025-09-24 14:42:11 +02:00
328 changed files with 28614 additions and 3068 deletions

View File

@@ -42,15 +42,19 @@ gd::String EventsCodeGenerator::GenerateRelationalOperatorCall(
const vector<gd::String>& arguments,
const gd::String& callStartString,
std::size_t startFromArgument) {
std::size_t relationalOperatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
std::size_t relationalOperatorIndex =
instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "relationalOperator") {
if (instrInfos.parameters.GetParameter(i).GetType() ==
"relationalOperator") {
relationalOperatorIndex = i;
}
}
// Ensure that there is at least one parameter after the relational operator
if (relationalOperatorIndex + 1 >= instrInfos.parameters.GetParametersCount()) {
if (relationalOperatorIndex + 1 >=
instrInfos.parameters.GetParametersCount()) {
ReportError();
return "";
}
@@ -87,20 +91,23 @@ gd::String EventsCodeGenerator::GenerateRelationalOperation(
const gd::String& relationalOperator,
const gd::String& lhs,
const gd::String& rhs) {
return lhs + " " + GenerateRelationalOperatorCodes(relationalOperator) + " " + rhs;
return lhs + " " + GenerateRelationalOperatorCodes(relationalOperator) + " " +
rhs;
}
const gd::String EventsCodeGenerator::GenerateRelationalOperatorCodes(const gd::String &operatorString) {
if (operatorString == "=") {
return "==";
}
if (operatorString != "<" && operatorString != ">" &&
operatorString != "<=" && operatorString != ">=" && operatorString != "!=" &&
operatorString != "startsWith" && operatorString != "endsWith" && operatorString != "contains") {
cout << "Warning: Bad relational operator: Set to == by default." << endl;
return "==";
}
return operatorString;
const gd::String EventsCodeGenerator::GenerateRelationalOperatorCodes(
const gd::String& operatorString) {
if (operatorString == "=") {
return "==";
}
if (operatorString != "<" && operatorString != ">" &&
operatorString != "<=" && operatorString != ">=" &&
operatorString != "!=" && operatorString != "startsWith" &&
operatorString != "endsWith" && operatorString != "contains") {
cout << "Warning: Bad relational operator: Set to == by default." << endl;
return "==";
}
return operatorString;
}
/**
@@ -124,7 +131,8 @@ gd::String EventsCodeGenerator::GenerateOperatorCall(
const gd::String& getterStartString,
std::size_t startFromArgument) {
std::size_t operatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "operator") {
operatorIndex = i;
@@ -195,7 +203,8 @@ gd::String EventsCodeGenerator::GenerateCompoundOperatorCall(
const gd::String& callStartString,
std::size_t startFromArgument) {
std::size_t operatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "operator") {
operatorIndex = i;
@@ -248,7 +257,8 @@ gd::String EventsCodeGenerator::GenerateMutatorCall(
const gd::String& callStartString,
std::size_t startFromArgument) {
std::size_t operatorIndex = instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument; i < instrInfos.parameters.GetParametersCount();
for (std::size_t i = startFromArgument;
i < instrInfos.parameters.GetParametersCount();
++i) {
if (instrInfos.parameters.GetParameter(i).GetType() == "operator") {
operatorIndex = i;
@@ -323,34 +333,42 @@ gd::String EventsCodeGenerator::GenerateConditionCode(
}
// Insert code only parameters and be sure there is no lack of parameter.
while (condition.GetParameters().size() < instrInfos.parameters.GetParametersCount()) {
while (condition.GetParameters().size() <
instrInfos.parameters.GetParametersCount()) {
vector<gd::Expression> parameters = condition.GetParameters();
parameters.push_back(gd::Expression(""));
condition.SetParameters(parameters);
}
// Verify that there are no mismatches between object type in parameters.
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount(); ++pNb) {
if (ParameterMetadata::IsObject(instrInfos.parameters.GetParameter(pNb).GetType())) {
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount();
++pNb) {
if (ParameterMetadata::IsObject(
instrInfos.parameters.GetParameter(pNb).GetType())) {
gd::String objectInParameter =
condition.GetParameter(pNb).GetPlainString();
const auto &expectedObjectType =
const auto& expectedObjectType =
instrInfos.parameters.GetParameter(pNb).GetExtraInfo();
const auto &actualObjectType =
const auto& actualObjectType =
GetObjectsContainersList().GetTypeOfObject(objectInParameter);
if (!GetObjectsContainersList().HasObjectOrGroupNamed(
objectInParameter)) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::UnknownObject, "",
objectInParameter, "");
gd::ProjectDiagnostic::ErrorType::UnknownObject,
"",
objectInParameter,
"");
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Unknown object - skipped. */";
} else if (!expectedObjectType.empty() &&
actualObjectType != expectedObjectType) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType, "",
actualObjectType, expectedObjectType, objectInParameter);
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType,
"",
actualObjectType,
expectedObjectType,
objectInParameter);
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Mismatched object type - skipped. */";
}
@@ -366,44 +384,46 @@ gd::String EventsCodeGenerator::GenerateConditionCode(
gd::String objectName = condition.GetParameter(0).GetPlainString();
if (!objectName.empty() && instrInfos.parameters.GetParametersCount() > 0) {
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(objectName, context.GetCurrentObject());
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
for (std::size_t i = 0; i < realObjects.size(); ++i) {
// Set up the context
gd::String objectType = GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
gd::String objectType =
GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
const ObjectMetadata& objInfo =
MetadataProvider::GetObjectMetadata(platform, objectType);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
// Prepare arguments and generate the condition whole code
vector<gd::String> arguments = GenerateParametersCodes(
condition.GetParameters(), instrInfos.parameters, context);
conditionCode += GenerateObjectCondition(realObjects[i],
objInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
// Prepare arguments and generate the condition whole code
vector<gd::String> arguments = GenerateParametersCodes(
condition.GetParameters(), instrInfos.parameters, context);
conditionCode += GenerateObjectCondition(realObjects[i],
objInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
context.SetNoCurrentObject();
context.SetNoCurrentObject();
}
}
} else if (instrInfos.IsBehaviorInstruction()) {
if (instrInfos.parameters.GetParametersCount() >= 2) {
const gd::String &objectName = condition.GetParameter(0).GetPlainString();
const gd::String &behaviorName =
const gd::String& objectName = condition.GetParameter(0).GetPlainString();
const gd::String& behaviorName =
condition.GetParameter(1).GetPlainString();
const gd::String &actualBehaviorType =
const gd::String& actualBehaviorType =
GetObjectsContainersList().GetTypeOfBehavior(behaviorName);
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
const BehaviorMetadata &autoInfo =
const BehaviorMetadata& autoInfo =
MetadataProvider::GetBehaviorMetadata(platform, actualBehaviorType);
for (std::size_t i = 0; i < realObjects.size(); ++i) {
@@ -415,15 +435,14 @@ gd::String EventsCodeGenerator::GenerateConditionCode(
// Prepare arguments and generate the whole condition code
vector<gd::String> arguments = GenerateParametersCodes(
condition.GetParameters(), instrInfos.parameters, context);
conditionCode += GenerateBehaviorCondition(
realObjects[i],
behaviorName,
autoInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
conditionCode += GenerateBehaviorCondition(realObjects[i],
behaviorName,
autoInfo,
arguments,
instrInfos,
returnBoolean,
condition.IsInverted(),
context);
context.SetNoCurrentObject();
}
@@ -493,26 +512,29 @@ gd::String EventsCodeGenerator::GenerateConditionsListCode(
}
bool EventsCodeGenerator::CheckBehaviorParameters(
const gd::Instruction &instruction,
const gd::InstructionMetadata &instrInfos) {
const gd::Instruction& instruction,
const gd::InstructionMetadata& instrInfos) {
bool isAnyBehaviorMissing = false;
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
instruction.GetParameters(), instrInfos.parameters,
[this, &isAnyBehaviorMissing,
&instrInfos](const gd::ParameterMetadata &parameterMetadata,
const gd::Expression &parameterValue, size_t parameterIndex,
const gd::String &lastObjectName, size_t lastObjectIndex) {
instruction.GetParameters(),
instrInfos.parameters,
[this, &isAnyBehaviorMissing, &instrInfos](
const gd::ParameterMetadata& parameterMetadata,
const gd::Expression& parameterValue,
size_t parameterIndex,
const gd::String& lastObjectName,
size_t lastObjectIndex) {
if (ParameterMetadata::IsBehavior(parameterMetadata.GetType())) {
const gd::String &behaviorName = parameterValue.GetPlainString();
const gd::String &actualBehaviorType =
const gd::String& behaviorName = parameterValue.GetPlainString();
const gd::String& actualBehaviorType =
GetObjectsContainersList().GetTypeOfBehaviorInObjectOrGroup(
lastObjectName, behaviorName);
const gd::String &expectedBehaviorType =
const gd::String& expectedBehaviorType =
parameterMetadata.GetExtraInfo();
if (!expectedBehaviorType.empty() &&
actualBehaviorType != expectedBehaviorType) {
const auto &objectParameterMetadata =
const auto& objectParameterMetadata =
instrInfos.GetParameter(lastObjectIndex);
// Event functions crash if some objects in a group are missing
// the required behaviors, since they lose reference to the original
@@ -523,10 +545,12 @@ bool EventsCodeGenerator::CheckBehaviorParameters(
isAnyBehaviorMissing = true;
}
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::MissingBehavior, "",
actualBehaviorType, expectedBehaviorType, lastObjectName);
if (diagnosticReport)
diagnosticReport->Add(projectDiagnostic);
gd::ProjectDiagnostic::ErrorType::MissingBehavior,
"",
actualBehaviorType,
expectedBehaviorType,
lastObjectName);
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
}
}
});
@@ -539,7 +563,8 @@ bool EventsCodeGenerator::CheckBehaviorParameters(
gd::String EventsCodeGenerator::GenerateActionCode(
gd::Instruction& action,
EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
gd::String actionCode;
const gd::InstructionMetadata& instrInfos =
@@ -564,33 +589,41 @@ gd::String EventsCodeGenerator::GenerateActionCode(
: instrInfos.codeExtraInformation.functionCallName;
// Be sure there is no lack of parameter.
while (action.GetParameters().size() < instrInfos.parameters.GetParametersCount()) {
while (action.GetParameters().size() <
instrInfos.parameters.GetParametersCount()) {
vector<gd::Expression> parameters = action.GetParameters();
parameters.push_back(gd::Expression(""));
action.SetParameters(parameters);
}
// Verify that there are no mismatches between object type in parameters.
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount(); ++pNb) {
if (ParameterMetadata::IsObject(instrInfos.parameters.GetParameter(pNb).GetType())) {
for (std::size_t pNb = 0; pNb < instrInfos.parameters.GetParametersCount();
++pNb) {
if (ParameterMetadata::IsObject(
instrInfos.parameters.GetParameter(pNb).GetType())) {
gd::String objectInParameter = action.GetParameter(pNb).GetPlainString();
const auto &expectedObjectType =
const auto& expectedObjectType =
instrInfos.parameters.GetParameter(pNb).GetExtraInfo();
const auto &actualObjectType =
const auto& actualObjectType =
GetObjectsContainersList().GetTypeOfObject(objectInParameter);
if (!GetObjectsContainersList().HasObjectOrGroupNamed(
objectInParameter)) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::UnknownObject, "",
objectInParameter, "");
gd::ProjectDiagnostic::ErrorType::UnknownObject,
"",
objectInParameter,
"");
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Unknown object - skipped. */";
} else if (!expectedObjectType.empty() &&
actualObjectType != expectedObjectType) {
gd::ProjectDiagnostic projectDiagnostic(
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType, "",
actualObjectType, expectedObjectType, objectInParameter);
gd::ProjectDiagnostic::ErrorType::MismatchedObjectType,
"",
actualObjectType,
expectedObjectType,
objectInParameter);
if (diagnosticReport) diagnosticReport->Add(projectDiagnostic);
return "/* Mismatched object type - skipped. */";
}
@@ -608,43 +641,46 @@ gd::String EventsCodeGenerator::GenerateActionCode(
if (instrInfos.parameters.GetParametersCount() > 0) {
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(objectName, context.GetCurrentObject());
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
for (std::size_t i = 0; i < realObjects.size(); ++i) {
// Setup context
gd::String objectType = GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
gd::String objectType =
GetObjectsContainersList().GetTypeOfObject(realObjects[i]);
const ObjectMetadata& objInfo =
MetadataProvider::GetObjectMetadata(platform, objectType);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
AddIncludeFiles(objInfo.includeFiles);
context.SetCurrentObject(realObjects[i]);
context.ObjectsListNeeded(realObjects[i]);
// Prepare arguments and generate the whole action code
vector<gd::String> arguments = GenerateParametersCodes(
action.GetParameters(), instrInfos.parameters, context);
actionCode += GenerateObjectAction(realObjects[i],
objInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName);
// Prepare arguments and generate the whole action code
vector<gd::String> arguments = GenerateParametersCodes(
action.GetParameters(), instrInfos.parameters, context);
actionCode += GenerateObjectAction(realObjects[i],
objInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName,
optionalAsyncCallbackId);
context.SetNoCurrentObject();
context.SetNoCurrentObject();
}
}
} else if (instrInfos.IsBehaviorInstruction()) {
if (instrInfos.parameters.GetParametersCount() >= 2) {
const gd::String &objectName = action.GetParameter(0).GetPlainString();
const gd::String &behaviorName = action.GetParameter(1).GetPlainString();
const gd::String &actualBehaviorType =
const gd::String& objectName = action.GetParameter(0).GetPlainString();
const gd::String& behaviorName = action.GetParameter(1).GetPlainString();
const gd::String& actualBehaviorType =
GetObjectsContainersList().GetTypeOfBehavior(behaviorName);
std::vector<gd::String> realObjects =
GetObjectsContainersList().ExpandObjectName(
objectName, context.GetCurrentObject());
const BehaviorMetadata &autoInfo =
const BehaviorMetadata& autoInfo =
MetadataProvider::GetBehaviorMetadata(platform, actualBehaviorType);
AddIncludeFiles(autoInfo.includeFiles);
@@ -656,15 +692,15 @@ gd::String EventsCodeGenerator::GenerateActionCode(
// Prepare arguments and generate the whole action code
vector<gd::String> arguments = GenerateParametersCodes(
action.GetParameters(), instrInfos.parameters, context);
actionCode +=
GenerateBehaviorAction(realObjects[i],
behaviorName,
autoInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName);
actionCode += GenerateBehaviorAction(realObjects[i],
behaviorName,
autoInfo,
functionCallName,
arguments,
instrInfos,
context,
optionalAsyncCallbackName,
optionalAsyncCallbackId);
context.SetNoCurrentObject();
}
@@ -676,7 +712,8 @@ gd::String EventsCodeGenerator::GenerateActionCode(
arguments,
instrInfos,
context,
optionalAsyncCallbackName);
optionalAsyncCallbackName,
optionalAsyncCallbackId);
}
return actionCode;
@@ -689,8 +726,8 @@ gd::String EventsCodeGenerator::GenerateLocalVariablesStackAccessor() {
}
gd::String EventsCodeGenerator::GenerateAnyOrSceneVariableGetter(
const gd::Expression &variableExpression,
EventsCodeGenerationContext &context) {
const gd::Expression& variableExpression,
EventsCodeGenerationContext& context) {
const auto variableName = gd::ExpressionVariableNameFinder::GetVariableName(
*variableExpression.GetRootNode());
@@ -701,8 +738,12 @@ gd::String EventsCodeGenerator::GenerateAnyOrSceneVariableGetter(
: "scenevar";
return gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, variableParameterType,
variableExpression.GetPlainString(), "", "AllowUndeclaredVariable");
*this,
context,
variableParameterType,
variableExpression.GetPlainString(),
"",
"AllowUndeclaredVariable");
}
const EventsCodeGenerator::CallbackDescriptor
@@ -749,6 +790,11 @@ EventsCodeGenerator::GenerateCallback(
AddCustomCodeOutsideMain(callbackCode);
const gd::String idToCallbackMapUpdate = GetCodeNamespaceAccessor() +
"idToCallbackMap.set(" + callbackID +
", " + callbackFunctionName + ");\n";
AddCustomCodeOutsideMain(idToCallbackMapUpdate);
std::set<gd::String> requiredObjects;
// Build the list of all objects required by the callback. Any object that has
// already been declared could have gone through previous object picking, so
@@ -808,13 +854,28 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
if (ParameterMetadata::IsExpression("number", metadata.GetType())) {
argOutput = gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, "number", parameter, lastObjectName, metadata.GetExtraInfo());
*this,
context,
"number",
parameter,
lastObjectName,
metadata.GetExtraInfo());
} else if (ParameterMetadata::IsExpression("string", metadata.GetType())) {
argOutput = gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, "string", parameter, lastObjectName, metadata.GetExtraInfo());
*this,
context,
"string",
parameter,
lastObjectName,
metadata.GetExtraInfo());
} else if (ParameterMetadata::IsExpression("variable", metadata.GetType())) {
argOutput = gd::ExpressionCodeGenerator::GenerateExpressionCode(
*this, context, metadata.GetType(), parameter, lastObjectName, metadata.GetExtraInfo());
*this,
context,
metadata.GetType(),
parameter,
lastObjectName,
metadata.GetExtraInfo());
} else if (ParameterMetadata::IsObject(metadata.GetType())) {
// It would be possible to run a gd::ExpressionCodeGenerator if later
// objects can have nested objects, or function returning objects.
@@ -849,7 +910,8 @@ gd::String EventsCodeGenerator::GenerateParameterCodes(
metadata.GetType() == "atlasResource" ||
metadata.GetType() == "spineResource" ||
// Deprecated, old parameter names:
metadata.GetType() == "password" || metadata.GetType() == "musicfile" ||
metadata.GetType() == "password" ||
metadata.GetType() == "musicfile" ||
metadata.GetType() == "soundfile") {
argOutput = "\"" + ConvertToString(parameter.GetPlainString()) + "\"";
} else if (metadata.GetType() == "mouse") {
@@ -999,7 +1061,8 @@ gd::String EventsCodeGenerator::GenerateEventsListCode(
for (std::size_t eId = 0; eId < events.size(); ++eId) {
auto& event = events[eId];
if (event.HasVariables()) {
GetProjectScopedContainers().GetVariablesContainersList().Push(event.GetVariables());
GetProjectScopedContainers().GetVariablesContainersList().Push(
event.GetVariables());
}
// Each event has its own context : Objects picked in an event are totally
@@ -1124,7 +1187,7 @@ gd::String EventsCodeGenerator::GenerateFreeCondition(
instrInfos.codeExtraInformation.functionCallName);
} else {
predicate = instrInfos.codeExtraInformation.functionCallName + "(" +
GenerateArgumentsList(arguments, 0) + ")";
GenerateArgumentsList(arguments, 0) + ")";
}
// Add logical not if needed
@@ -1168,7 +1231,7 @@ gd::String EventsCodeGenerator::GenerateObjectCondition(
instrInfos, arguments, objectFunctionCallNamePart, 1);
} else {
predicate = objectFunctionCallNamePart + "(" +
GenerateArgumentsList(arguments, 1) + ")";
GenerateArgumentsList(arguments, 1) + ")";
}
if (conditionInverted) predicate = GenerateNegatedPredicate(predicate);
@@ -1200,18 +1263,20 @@ gd::String EventsCodeGenerator::GenerateBehaviorCondition(
}
gd::String EventsCodeGenerator::GenerateFreeAction(
const gd::String& functionCallName,
const gd::String& functionCallName,
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
// Generate call
gd::String call;
if (instrInfos.codeExtraInformation.type == "number" ||
instrInfos.codeExtraInformation.type == "string" ||
// Boolean actions declared with addExpressionAndConditionAndAction uses
// MutatorAndOrAccessor even though they don't declare an operator parameter.
// Boolean operators are only used with SetMutators or SetCustomCodeGenerator.
// MutatorAndOrAccessor even though they don't declare an operator
// parameter. Boolean operators are only used with SetMutators or
// SetCustomCodeGenerator.
(instrInfos.codeExtraInformation.type == "boolean" &&
instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::AccessType::Mutators)) {
@@ -1224,23 +1289,19 @@ gd::String EventsCodeGenerator::GenerateFreeAction(
instrInfos.codeExtraInformation.optionalAssociatedInstruction);
else if (instrInfos.codeExtraInformation.accessType ==
gd::InstructionMetadata::ExtraInformation::Mutators)
call =
GenerateMutatorCall(instrInfos,
arguments,
functionCallName);
call = GenerateMutatorCall(instrInfos, arguments, functionCallName);
else
call = GenerateCompoundOperatorCall(
instrInfos,
arguments,
functionCallName);
call =
GenerateCompoundOperatorCall(instrInfos, arguments, functionCallName);
} else {
call = functionCallName + "(" +
GenerateArgumentsList(arguments) + ")";
call = functionCallName + "(" + GenerateArgumentsList(arguments) + ")";
}
if (!optionalAsyncCallbackName.empty())
if (!optionalAsyncCallbackName.empty() && !optionalAsyncCallbackId.empty()) {
call = "runtimeScene.getAsyncTasksManager().addTask(" + call + ", " +
optionalAsyncCallbackName + ")";
optionalAsyncCallbackName + ", " + optionalAsyncCallbackId +
", asyncObjectsList)";
}
return call + ";\n";
}
@@ -1252,7 +1313,8 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
// Create call
gd::String call;
if ((instrInfos.codeExtraInformation.type == "number" ||
@@ -1293,7 +1355,8 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
// Create call
gd::String call;
if ((instrInfos.codeExtraInformation.type == "number" ||
@@ -1308,17 +1371,13 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
2);
else
call = GenerateCompoundOperatorCall(
instrInfos,
arguments,
functionCallName,
2);
instrInfos, arguments, functionCallName, 2);
return "For each picked object \"" + objectName + "\", call " + call +
" for behavior \"" + behaviorName + "\".\n";
} else {
gd::String argumentsStr = GenerateArgumentsList(arguments, 2);
call = functionCallName + "(" +
argumentsStr + ")";
call = functionCallName + "(" + argumentsStr + ")";
return "For each picked object \"" + objectName + "\", call " + call + "(" +
argumentsStr + ")" + " for behavior \"" + behaviorName + "\"" +
@@ -1373,42 +1432,47 @@ gd::String EventsCodeGenerator::GenerateArgumentsList(
return argumentsStr;
}
gd::String EventsCodeGenerator::GeneratePropertyGetter(const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
gd::String EventsCodeGenerator::GeneratePropertyGetter(
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
return "getProperty" + property.GetName() + "As" + type + "()";
}
gd::String EventsCodeGenerator::GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property) {
return "getProperty" + property.GetName() + "()";
}
gd::String EventsCodeGenerator::GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode) {
return "setProperty" + property.GetName() + "(" + operandCode + ")";
}
}
gd::String EventsCodeGenerator::GenerateParameterGetter(const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
gd::String EventsCodeGenerator::GenerateParameterGetter(
const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) {
return "getParameter" + parameter.GetName() + "As" + type + "()";
}
gd::String EventsCodeGenerator::GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata &parameter) {
return "getParameter" + parameter.GetName() + "()";
}
const gd::ParameterMetadata& parameter) {
return "getParameter" + parameter.GetName() + "()";
}
EventsCodeGenerator::EventsCodeGenerator(const gd::Project& project_,
const gd::Layout& layout,
const gd::Platform& platform_)
: platform(platform_),
projectScopedContainers(gd::ProjectScopedContainers::MakeNewProjectScopedContainersForProjectAndLayout(project_, layout)),
projectScopedContainers(
gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForProjectAndLayout(project_,
layout)),
hasProjectAndLayout(true),
project(&project_),
scene(&layout),
@@ -1417,7 +1481,7 @@ EventsCodeGenerator::EventsCodeGenerator(const gd::Project& project_,
maxCustomConditionsDepth(0),
maxConditionsListsSize(0),
eventsListNextUniqueId(0),
diagnosticReport(nullptr){};
diagnosticReport(nullptr) {};
EventsCodeGenerator::EventsCodeGenerator(
const gd::Platform& platform_,
@@ -1432,6 +1496,6 @@ EventsCodeGenerator::EventsCodeGenerator(
maxCustomConditionsDepth(0),
maxConditionsListsSize(0),
eventsListNextUniqueId(0),
diagnosticReport(nullptr){};
diagnosticReport(nullptr) {};
} // namespace gd

View File

@@ -9,9 +9,9 @@
#include <utility>
#include <vector>
#include "GDCore/Events/CodeGeneration/DiagnosticReport.h"
#include "GDCore/Events/Event.h"
#include "GDCore/Events/Instruction.h"
#include "GDCore/Events/CodeGeneration/DiagnosticReport.h"
#include "GDCore/Project/ProjectScopedContainers.h"
#include "GDCore/String.h"
@@ -62,7 +62,7 @@ class GD_CORE_API EventsCodeGenerator {
EventsCodeGenerator(
const gd::Platform& platform,
const gd::ProjectScopedContainers& projectScopedContainers_);
virtual ~EventsCodeGenerator(){};
virtual ~EventsCodeGenerator() {};
/**
* \brief Preprocess an events list (replacing for example links with the
@@ -160,7 +160,8 @@ class GD_CORE_API EventsCodeGenerator {
gd::String GenerateActionCode(
gd::Instruction& action,
EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
struct CallbackDescriptor {
CallbackDescriptor(const gd::String functionName_,
@@ -168,7 +169,7 @@ class GD_CORE_API EventsCodeGenerator {
const std::set<gd::String> requiredObjects_)
: functionName(functionName_),
argumentsList(argumentsList_),
requiredObjects(requiredObjects_){};
requiredObjects(requiredObjects_) {};
/**
* The name by which the function can be invoked.
*/
@@ -338,9 +339,9 @@ class GD_CORE_API EventsCodeGenerator {
}
/**
* @brief Give access to the project scoped containers as code generation might
* push and pop variable containers (for local variables).
* This could be passed as a parameter recursively in code generation, but this requires
* @brief Give access to the project scoped containers as code generation
* might push and pop variable containers (for local variables). This could be
* passed as a parameter recursively in code generation, but this requires
* heavy refactoring. Instead, we use this single instance.
*/
gd::ProjectScopedContainers& GetProjectScopedContainers() {
@@ -387,9 +388,7 @@ class GD_CORE_API EventsCodeGenerator {
diagnosticReport = diagnosticReport_;
}
gd::DiagnosticReport* GetDiagnosticReport() {
return diagnosticReport;
}
gd::DiagnosticReport* GetDiagnosticReport() { return diagnosticReport; }
/**
* \brief Generate the full name for accessing to a boolean variable used for
@@ -513,16 +512,16 @@ class GD_CORE_API EventsCodeGenerator {
* \brief Generate an any variable getter that fallbacks on scene variable for
* compatibility reason.
*/
gd::String
GenerateAnyOrSceneVariableGetter(const gd::Expression &variableExpression,
EventsCodeGenerationContext &context);
gd::String GenerateAnyOrSceneVariableGetter(
const gd::Expression& variableExpression,
EventsCodeGenerationContext& context);
virtual gd::String GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode);
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode);
protected:
protected:
virtual const gd::String GenerateRelationalOperatorCodes(
const gd::String& operatorString);
@@ -643,16 +642,16 @@ protected:
gd::EventsCodeGenerationContext& context);
virtual gd::String GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property);
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property);
virtual gd::String GenerateParameterGetter(
const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context);
virtual gd::String
GenerateParameterGetterWithoutCasting(const gd::ParameterMetadata &parameter);
virtual gd::String GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata& parameter);
/**
* \brief Generate the code to reference an object which is
@@ -769,7 +768,8 @@ protected:
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
virtual gd::String GenerateObjectAction(
const gd::String& objectName,
@@ -778,7 +778,8 @@ protected:
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
virtual gd::String GenerateBehaviorAction(
const gd::String& objectName,
@@ -788,7 +789,8 @@ protected:
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "");
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "");
gd::String GenerateRelationalOperatorCall(
const gd::InstructionMetadata& instrInfos,
@@ -837,9 +839,8 @@ protected:
virtual gd::String GenerateGetBehaviorNameCode(
const gd::String& behaviorName);
bool CheckBehaviorParameters(
const gd::Instruction &instruction,
const gd::InstructionMetadata &instrInfos);
bool CheckBehaviorParameters(const gd::Instruction& instruction,
const gd::InstructionMetadata& instrInfos);
const gd::Platform& platform; ///< The platform being used.
@@ -876,4 +877,3 @@ protected:
};
} // namespace gd

View File

@@ -293,6 +293,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.AddCodeOnlyParameter("currentScene", "")
.MarkAsAdvanced();
obj.AddAction(
"RotateTowardObject",
_("Rotate toward another object"),
_("Rotate an object towards another object, with the specified speed. "
"Note that if multiple instances of the target object are picked, "
"only the first one will be used. Use a For Each event or actions "
"like \"Pick nearest object\", \"Pick a random object\" to refine "
"the choice of the target object."),
_("Rotate _PARAM0_ towards _PARAM1_ at speed _PARAM2_ deg/second"),
_("Angle"),
"res/actions/rotate24_black.png",
"res/actions/rotate_black.png")
.AddParameter("object", _("Object"))
.AddParameter("objectPtr", _("Target object"))
.AddParameter("expression", _("Angular speed (in degrees per second)"))
.SetParameterLongDescription(_("Enter 0 for an immediate rotation."))
.AddCodeOnlyParameter("currentScene", "")
.MarkAsAdvanced();
obj.AddAction(
"AddForceXY",
_("Add a force"),
@@ -1617,7 +1636,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
extension
.AddAction("AjoutObjConcern",
_("Pick all instances"),
_("Pick all object instances"),
_("Pick all instances of the specified object(s). When you "
"pick all instances, "
"the next conditions and actions of this event work on all "
@@ -1631,20 +1650,34 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.MarkAsAdvanced();
extension
.AddAction(
"AjoutHasard",
_("Pick a random object"),
_("Pick one object from all the specified objects. When an object "
"is picked, the next conditions and actions of this event work "
"only on that object."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/actions/ajouthasard24.png",
"res/actions/ajouthasard.png")
.AddAction("AjoutHasard",
_("Pick a random object"),
_("Pick one instance from all the specified objects. When an "
"instance is picked, the next conditions and actions of "
"this event work only on that object instance."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/actions/ajouthasard24.png",
"res/actions/ajouthasard.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectList", _("Object"))
.MarkAsSimple();
extension
.AddAction(
"PickNearest",
_("Pick nearest object"),
_("Pick the instance of this object that is nearest to the specified "
"position."),
_("Pick the _PARAM0_ that is nearest to _PARAM1_;_PARAM2_"),
_("Objects"),
"res/conditions/distance24.png",
"res/conditions/distance.png")
.AddParameter("objectList", _("Object"))
.AddParameter("expression", _("X position"))
.AddParameter("expression", _("Y position"))
.MarkAsSimple();
extension
.AddAction(
"MoveObjects",
@@ -1694,11 +1727,12 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
extension
.AddCondition(
"AjoutObjConcern",
_("Pick all objects"),
_("Pick all the specified objects. When you pick all objects, "
_("Pick all object instances"),
_("Pick all instances of the specified object(s). When you "
"pick all instances, "
"the next conditions and actions of this event work on all "
"of them."),
_("Pick all _PARAM1_ objects"),
_("Pick all instances of _PARAM1_"),
_("Objects"),
"res/conditions/add24.png",
"res/conditions/add.png")
@@ -1707,16 +1741,15 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.MarkAsAdvanced();
extension
.AddCondition(
"AjoutHasard",
_("Pick a random object"),
_("Pick one object from all the specified objects. When an object "
"is picked, the next conditions and actions of this event work "
"only on that object."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/conditions/ajouthasard24.png",
"res/conditions/ajouthasard.png")
.AddCondition("AjoutHasard",
_("Pick a random object"),
_("Pick one instance from all the specified objects. When "
"an instance is picked, the next conditions and actions "
"of this event work only on that object instance."),
_("Pick a random _PARAM1_"),
_("Objects"),
"res/conditions/ajouthasard24.png",
"res/conditions/ajouthasard.png")
.AddCodeOnlyParameter("objectsContext", "")
.AddParameter("objectList", _("Object"))
.MarkAsSimple();
@@ -1725,9 +1758,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsBaseObjectExtension(
.AddCondition(
"PickNearest",
_("Pick nearest object"),
_("Pick the object of this type that is nearest to the specified "
"position. If the condition is inverted, the object farthest from "
"the specified position is picked instead."),
_("Pick the instance of this object that is nearest to the specified "
"position. If the condition is inverted, the instance farthest "
"from the specified position is picked instead."),
_("Pick the _PARAM0_ that is nearest to _PARAM1_;_PARAM2_"),
_("Objects"),
"res/conditions/distance24.png",

View File

@@ -42,13 +42,17 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFileExtension(
extension
.AddAction(
"LoadFile",
_("Load a storage in memory"),
_("This action loads the specified storage in memory, so you can "
"write and read it.\nYou can open and write without using this "
"action, but it will be slower.\nIf you use this action, do not "
"forget to unload the storage from memory."),
_("Load storage _PARAM0_ in memory"),
"",
_("Manually preload a storage in memory"),
_("Forces the specified storage to be loaded and kept in "
"memory, allowing faster reads/writes. "
"However, it requires manual management: if you use this "
"action, you *must* also unload the storage manually when "
"it's no longer needed to ensure data is persisted.\n\n"
"Unless you have a specific performance need, avoid using this "
"action. The system already handles loading/unloading "
"automatically."),
_("Load data storage _PARAM0_ in memory"),
_("Advanced"),
"res/actions/fichier24.png",
"res/actions/fichier.png")
.AddParameter("string", _("Storage name"))
@@ -56,11 +60,11 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsFileExtension(
extension
.AddAction("UnloadFile",
_("Close a storage"),
_("This action closes the structured data previously loaded "
_("Manually unload and persist a storage"),
_("Close the specified storage previously loaded "
"in memory, saving all changes made."),
_("Close structured data _PARAM0_"),
"",
_("Unload and persist data storage _PARAM0_"),
_("Advanced"),
"res/actions/fichier24.png",
"res/actions/fichier.png")
.AddParameter("string", _("Storage name"))

View File

@@ -24,7 +24,7 @@ The rest of this page is an introduction to the main concepts of GDevelop archit
Extensions do have the same distinction between the "**IDE**" part and the "**Runtime**" part. For example, most extensions have:
- A file called [`JsExtension.js`(https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/JsExtension.js)], which contains the _declaration_ of the extension for the **IDE**
- A file called [`JsExtension.js`](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/JsExtension.js), which contains the _declaration_ of the extension for the **IDE**
- One or more files implementing the feature for the game, in other words for **Runtime**. This can be a [Runtime Object](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/dummyruntimeobject.ts) or a [Runtime Behavior](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/dummyruntimebehavior.ts), [functions called by actions or conditions](https://github.com/4ian/GDevelop/blob/master/Extensions/ExampleJsExtension/examplejsextensiontools.ts) or by the game engine.
### "Runtime" and "IDE" difference using an example: the `gd::Variable` class

View File

@@ -97,6 +97,14 @@ namespace gdjs {
oldObjectData: Object3DData,
newObjectData: Object3DData
): boolean {
this.updateOriginalDimensionsFromObjectData(oldObjectData, newObjectData);
return true;
}
updateOriginalDimensionsFromObjectData(
oldObjectData: Object3DData,
newObjectData: Object3DData
): void {
// There is no need to check if they changed because events can't modify them.
this._setOriginalWidth(
getValidDimensionValue(newObjectData.content.width)
@@ -107,12 +115,13 @@ namespace gdjs {
this._setOriginalDepth(
getValidDimensionValue(newObjectData.content.depth)
);
return true;
}
getNetworkSyncData(): Object3DNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): Object3DNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
z: this.getZ(),
d: this.getDepth(),
rx: this.getRotationX(),
@@ -123,8 +132,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: Object3DNetworkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: Object3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
if (networkSyncData.rx !== undefined)

View File

@@ -452,9 +452,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): Cube3DObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): Cube3DObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
mt: this._materialType,
fo: this._facesOrientation,
bfu: this._backFaceUpThroughWhichAxisRotation,
@@ -466,9 +468,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: Cube3DObjectNetworkSyncData
networkSyncData: Cube3DObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;

View File

@@ -1,12 +1,16 @@
namespace gdjs {
type CustomObject3DNetworkSyncDataType = CustomObjectNetworkSyncDataType & {
type CustomObject3DNetworkSyncDataType = {
z: float;
d: float;
rx: float;
ry: float;
ifz: boolean;
ccz: float;
};
type CustomObject3DNetworkSyncData = CustomObjectNetworkSyncData &
CustomObject3DNetworkSyncDataType;
/**
* Base class for 3D custom objects.
*/
@@ -79,21 +83,25 @@ namespace gdjs {
this.flipZ(!!initialInstanceData.flippedZ);
}
getNetworkSyncData(): CustomObject3DNetworkSyncDataType {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): CustomObject3DNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
z: this.getZ(),
d: this.getDepth(),
rx: this.getRotationX(),
ry: this.getRotationY(),
ifz: this.isFlippedZ(),
ccz: this._customCenterZ,
};
}
updateFromNetworkSyncData(
networkSyncData: CustomObject3DNetworkSyncDataType
networkSyncData: CustomObject3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.z !== undefined) this.setZ(networkSyncData.z);
if (networkSyncData.d !== undefined) this.setDepth(networkSyncData.d);
if (networkSyncData.rx !== undefined)
@@ -101,6 +109,8 @@ namespace gdjs {
if (networkSyncData.ry !== undefined)
this.setRotationY(networkSyncData.ry);
if (networkSyncData.ifz !== undefined) this.flipZ(networkSyncData.ifz);
if (networkSyncData.ccz !== undefined)
this._customCenterZ = networkSyncData.ccz;
}
/**

View File

@@ -23,6 +23,8 @@ namespace gdjs {
this._threeGroup = new THREE.Group();
this._threeGroup.rotation.order = 'ZYX';
//@ts-ignore
this._threeGroup.gdjsRuntimeObject = object;
const layer = parent.getLayer('');
if (layer) {

View File

@@ -1901,6 +1901,11 @@ module.exports = {
.getOrCreate('density')
.setValue('0.0012')
.setLabel(_('Density'))
.setDescription(
_(
'Density of the fog. Usual values are between 0.0005 (far away) and 0.005 (very thick fog).'
)
)
.setType('number');
}
{

View File

@@ -8,6 +8,7 @@ namespace gdjs {
anis: Model3DAnimation[];
ai: integer;
ass: float;
aet: float;
ap: boolean;
cfd: float;
};
@@ -152,6 +153,14 @@ namespace gdjs {
}
}
override updateOriginalDimensionsFromObjectData(
oldObjectData: Object3DData,
newObjectData: Object3DData
): void {
// Original dimensions must not be reset by `super.updateFromObjectData`.
// `_updateModel` has a different logic to evaluate them using `keepAspectRatio`.
}
updateFromObjectData(
oldObjectData: Model3DObjectData,
newObjectData: Model3DObjectData
@@ -181,8 +190,14 @@ namespace gdjs {
oldObjectData.content.keepAspectRatio !==
newObjectData.content.keepAspectRatio ||
oldObjectData.content.materialType !==
newObjectData.content.materialType
newObjectData.content.materialType ||
oldObjectData.content.centerLocation !==
newObjectData.content.centerLocation
) {
// The center is applied to the model by `_updateModel`.
this._centerPoint = getPointForLocation(
newObjectData.content.centerLocation
);
this._updateModel(newObjectData);
}
if (
@@ -192,14 +207,7 @@ namespace gdjs {
this._originPoint = getPointForLocation(
newObjectData.content.originLocation
);
}
if (
oldObjectData.content.centerLocation !==
newObjectData.content.centerLocation
) {
this._centerPoint = getPointForLocation(
newObjectData.content.centerLocation
);
this._renderer.updatePosition();
}
if (
oldObjectData.content.isCastingShadow !==
@@ -216,24 +224,28 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): Model3DObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): Model3DObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
mt: this._materialType,
op: this._originPoint,
cp: this._centerPoint,
anis: this._animations,
ai: this._currentAnimationIndex,
ass: this._animationSpeedScale,
aet: this.getAnimationElapsedTime(),
ap: this._animationPaused,
cfd: this._crossfadeDuration,
};
}
updateFromNetworkSyncData(
networkSyncData: Model3DObjectNetworkSyncData
networkSyncData: Model3DObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.mt !== undefined) {
this._materialType = networkSyncData.mt;
@@ -247,11 +259,14 @@ namespace gdjs {
if (networkSyncData.anis !== undefined) {
this._animations = networkSyncData.anis;
}
if (networkSyncData.ass !== undefined) {
this.setAnimationSpeedScale(networkSyncData.ass);
}
if (networkSyncData.ai !== undefined) {
this.setAnimationIndex(networkSyncData.ai);
}
if (networkSyncData.ass !== undefined) {
this.setAnimationSpeedScale(networkSyncData.ass);
if (networkSyncData.aet !== undefined) {
this.setAnimationElapsedTime(networkSyncData.aet);
}
if (networkSyncData.ap !== undefined) {
if (networkSyncData.ap !== this.isAnimationPaused()) {
@@ -273,14 +288,17 @@ namespace gdjs {
const rotationX = objectData.content.rotationX || 0;
const rotationY = objectData.content.rotationY || 0;
const rotationZ = objectData.content.rotationZ || 0;
const width = objectData.content.width || 100;
const height = objectData.content.height || 100;
const depth = objectData.content.depth || 100;
const keepAspectRatio = objectData.content.keepAspectRatio;
this._renderer._updateModel(
rotationX,
rotationY,
rotationZ,
this.getOriginalWidth(),
this.getOriginalHeight(),
this.getOriginalDepth(),
width,
height,
depth,
keepAspectRatio
);
}

View File

@@ -233,6 +233,10 @@ namespace gdjs {
this._object._setOriginalWidth(scaleRatio * modelWidth);
this._object._setOriginalHeight(scaleRatio * modelHeight);
this._object._setOriginalDepth(scaleRatio * modelDepth);
} else {
this._object._setOriginalWidth(originalWidth);
this._object._setOriginalHeight(originalHeight);
this._object._setOriginalDepth(originalDepth);
}
}
@@ -286,6 +290,7 @@ namespace gdjs {
this.get3DRendererObject().remove(this._threeObject);
this.get3DRendererObject().add(threeObject);
this._threeObject = threeObject;
this.updatePosition();
this._updateShadow();
// Start the current animation on the new 3D object.

View File

@@ -40,6 +40,19 @@ describe('gdjs.AnchorRuntimeBehavior', () => {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -145,9 +145,11 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): BBTextObjectNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BBTextObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
text: this._text,
o: this._opacity,
c: this._color,
@@ -162,9 +164,10 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: BBTextObjectNetworkSyncData
networkSyncData: BBTextObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (this._text !== undefined) {
this.setBBText(networkSyncData.text);
}

View File

@@ -155,9 +155,11 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): BitmapTextObjectNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BitmapTextObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
text: this._text,
opa: this._opacity,
tint: this._tint,
@@ -172,9 +174,10 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: BitmapTextObjectNetworkSyncData
networkSyncData: BitmapTextObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (this._text !== undefined) {
this.setText(networkSyncData.text);
}

View File

@@ -21,7 +21,9 @@ module.exports = {
.setExtensionInformation(
'DebuggerTools',
_('Debugger Tools'),
_('Allow to interact with the editor debugger from the game.'),
_(
'Allow to interact with the editor debugger from the game (notably: enable 2D debug draw, log a message in the debugger console).'
),
'Arthur Pacaud (arthuro555), Aurélien Vivet (Bouh)',
'MIT'
)

View File

@@ -12,7 +12,8 @@ This project is released under the MIT License.
#include "GDCore/Tools/Localization.h"
void DestroyOutsideBehavior::InitializeContent(gd::SerializerElement& content) {
content.SetAttribute("extraBorder", 300);
content.SetAttribute("extraBorder", 200);
content.SetAttribute("unseenGraceDistance", 10000);
}
#if defined(GD_IDE_ONLY)
@@ -27,7 +28,15 @@ DestroyOutsideBehavior::GetProperties(
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetLabel(_("Deletion margin"))
.SetDescription(_("Margin before deleting the object, in pixels"));
.SetDescription(_("Margin before deleting the object, in pixels."));
properties["unseenGraceDistance"]
.SetValue(gd::String::From(
behaviorContent.GetDoubleAttribute("unseenGraceDistance", 0)))
.SetType("Number")
.SetMeasurementUnit(gd::MeasurementUnit::GetPixel())
.SetLabel(_("Unseen object grace distance"))
.SetDescription(_("If the object hasn't been visible yet, don't delete it until it travels this far beyond the screen (in pixels). Useful to avoid objects being deleted before they are visible when they spawn."));
return properties;
}
@@ -38,6 +47,8 @@ bool DestroyOutsideBehavior::UpdateProperty(
const gd::String& value) {
if (name == "extraBorder")
behaviorContent.SetAttribute("extraBorder", value.To<double>());
else if (name == "unseenGraceDistance")
behaviorContent.SetAttribute("unseenGraceDistance", value.To<double>());
else
return false;

View File

@@ -6,6 +6,7 @@ This project is released under the MIT License.
*/
#include "DestroyOutsideBehavior.h"
#include "GDCore/Extensions/Metadata/MultipleInstructionMetadata.h"
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/Project/BehaviorsSharedData.h"
#include "GDCore/Tools/Localization.h"
@@ -19,11 +20,10 @@ void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
"outside of the bounds of the 2D camera. Useful for 2D bullets or "
"other short-lived objects. Don't use it for 3D objects in a "
"FPS/TPS game or any game with a camera not being a top view "
"(for 3D objects, prefer comparing "
"the position, for example Z position to see if an object goes "
"outside of the bound of the map). Be careful when using this "
"behavior because if the object appears outside of the screen, it "
"will be immediately removed."),
"(for 3D objects, prefer comparing the position, for example Z "
"position to see if an object goes outside of the bound of the "
"map). If the object appears outside of the screen, it's not "
"removed unless it goes beyond the unseen object grace distance."),
"Florian Rival",
"Open source (MIT License)")
.SetCategory("Game mechanic")
@@ -44,34 +44,39 @@ void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
std::shared_ptr<gd::BehaviorsSharedData>())
.SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden);
aut.AddCondition("ExtraBorder",
_("Additional border (extra distance before deletion)"),
_("Compare the extra distance (in pixels) the object must "
"travel beyond the screen before it gets deleted."),
_("the additional border"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png",
"CppPlatform/Extensions/destroyoutsideicon16.png")
aut.AddExpressionAndConditionAndAction(
"number",
"ExtraBorder",
_("Additional border (extra distance before deletion)"),
_("the extra distance (in pixels) the object must "
"travel beyond the screen before it gets deleted"),
_("the additional border"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "DestroyOutside")
.UseStandardRelationalOperatorParameters(
"number", gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced()
.SetFunctionName("GetExtraBorder");
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced();
aut.AddAction("ExtraBorder",
_("Additional border (extra distance before deletion)"),
_("Change the extra distance (in pixels) the object must "
"travel beyond the screen before it gets deleted."),
_("the additional border"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png",
"CppPlatform/Extensions/destroyoutsideicon16.png")
// Deprecated:
aut.AddDuplicatedAction("ExtraBorder", "DestroyOutside::SetExtraBorder")
.SetHidden();
aut.AddDuplicatedCondition("ExtraBorder", "DestroyOutside::ExtraBorder")
.SetHidden();
aut.AddExpressionAndConditionAndAction(
"number",
"UnseenGraceDistance",
_("Unseen object grace distance"),
_("the grace distance (in pixels) before deleting the object if it "
"has "
"never been visible on the screen. Useful to avoid objects being "
"deleted before they are visible when they spawn"),
_("the unseen grace distance"),
_("Destroy outside configuration"),
"CppPlatform/Extensions/destroyoutsideicon24.png")
.AddParameter("object", _("Object"))
.AddParameter("behavior", _("Behavior"), "DestroyOutside")
.UseStandardOperatorParameters("number",
gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced()
.SetFunctionName("SetExtraBorder")
.SetGetter("GetExtraBorder");
.UseStandardParameters("number", gd::ParameterOptions::MakeNewOptions())
.MarkAsAdvanced();
}

View File

@@ -5,11 +5,11 @@ Copyright (c) 2014-2016 Florian Rival (Florian.Rival@gmail.com)
This project is released under the MIT License.
*/
#if defined(GD_IDE_ONLY)
#include <iostream>
#include "GDCore/Extensions/PlatformExtension.h"
#include "GDCore/Tools/Localization.h"
#include <iostream>
void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension);
/**
@@ -29,19 +29,36 @@ class DestroyOutsideBehaviorJsExtension : public gd::PlatformExtension {
"Extensions/DestroyOutsideBehavior/"
"destroyoutsideruntimebehavior.js");
GetAllExpressionsForBehavior(
"DestroyOutsideBehavior::DestroyOutside")["ExtraBorder"]
.SetFunctionName("getExtraBorder");
GetAllConditionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::ExtraBorder"]
.SetFunctionName("getExtraBorder");
GetAllActionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::SetExtraBorder"]
.SetFunctionName("setExtraBorder")
.SetGetter("getExtraBorder");
// Deprecated:
GetAllConditionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::ExtraBorder"]
.SetFunctionName("getExtraBorder")
.SetIncludeFile(
"Extensions/DestroyOutsideBehavior/"
"destroyoutsideruntimebehavior.js");
.SetFunctionName("getExtraBorder");
GetAllActionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::ExtraBorder"]
.SetFunctionName("setExtraBorder")
.SetGetter("getExtraBorder")
.SetIncludeFile(
"Extensions/DestroyOutsideBehavior/"
"destroyoutsideruntimebehavior.js");
.SetGetter("getExtraBorder");
GetAllExpressionsForBehavior(
"DestroyOutsideBehavior::DestroyOutside")["UnseenGraceDistance"]
.SetFunctionName("getUnseenGraceDistance");
GetAllConditionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::UnseenGraceDistance"]
.SetFunctionName("getUnseenGraceDistance");
GetAllActionsForBehavior("DestroyOutsideBehavior::DestroyOutside")
["DestroyOutsideBehavior::DestroyOutside::SetUnseenGraceDistance"]
.SetFunctionName("setUnseenGraceDistance")
.SetGetter("getUnseenGraceDistance");
GD_COMPLETE_EXTENSION_COMPILATION_INFORMATION();
};

View File

@@ -8,7 +8,9 @@ namespace gdjs {
* The DestroyOutsideRuntimeBehavior represents a behavior that destroys the object when it leaves the screen.
*/
export class DestroyOutsideRuntimeBehavior extends gdjs.RuntimeBehavior {
_extraBorder: any;
_extraBorder: float;
_unseenGraceDistance: float;
_hasBeenOnScreen: boolean;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -17,12 +19,20 @@ namespace gdjs {
) {
super(instanceContainer, behaviorData, owner);
this._extraBorder = behaviorData.extraBorder || 0;
this._unseenGraceDistance = behaviorData.unseenGraceDistance || 0;
this._hasBeenOnScreen = false;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
if (oldBehaviorData.extraBorder !== newBehaviorData.extraBorder) {
this._extraBorder = newBehaviorData.extraBorder;
}
if (
oldBehaviorData.unseenGraceDistance !==
newBehaviorData.unseenGraceDistance
) {
this._unseenGraceDistance = newBehaviorData.unseenGraceDistance;
}
return true;
}
@@ -35,23 +45,47 @@ namespace gdjs {
const ocy = this.owner.getDrawableY() + this.owner.getCenterY();
const layer = instanceContainer.getLayer(this.owner.getLayer());
const boundingCircleRadius = Math.sqrt(ow * ow + oh * oh) / 2.0;
const cameraLeft = layer.getCameraX() - layer.getCameraWidth() / 2;
const cameraRight = layer.getCameraX() + layer.getCameraWidth() / 2;
const cameraTop = layer.getCameraY() - layer.getCameraHeight() / 2;
const cameraBottom = layer.getCameraY() + layer.getCameraHeight() / 2;
if (
ocx + boundingCircleRadius + this._extraBorder <
layer.getCameraX() - layer.getCameraWidth() / 2 ||
ocx - boundingCircleRadius - this._extraBorder >
layer.getCameraX() + layer.getCameraWidth() / 2 ||
ocy + boundingCircleRadius + this._extraBorder <
layer.getCameraY() - layer.getCameraHeight() / 2 ||
ocy - boundingCircleRadius - this._extraBorder >
layer.getCameraY() + layer.getCameraHeight() / 2
ocx + boundingCircleRadius + this._extraBorder < cameraLeft ||
ocx - boundingCircleRadius - this._extraBorder > cameraRight ||
ocy + boundingCircleRadius + this._extraBorder < cameraTop ||
ocy - boundingCircleRadius - this._extraBorder > cameraBottom
) {
//We are outside the camera area.
this.owner.deleteFromScene();
if (this._hasBeenOnScreen) {
// Object is outside the camera area and object was previously seen inside it:
// delete it now.
this.owner.deleteFromScene();
} else if (
ocx + boundingCircleRadius + this._unseenGraceDistance < cameraLeft ||
ocx - boundingCircleRadius - this._unseenGraceDistance >
cameraRight ||
ocy + boundingCircleRadius + this._unseenGraceDistance < cameraTop ||
ocy - boundingCircleRadius - this._unseenGraceDistance > cameraBottom
) {
// Object is outside the camera area and also outside the grace distance:
// force deletion.
this.owner.deleteFromScene();
} else {
// Object is outside the camera area but inside the grace distance
// and was never seen inside the camera area: don't delete it yet.
}
} else {
this._hasBeenOnScreen = true;
}
}
/**
* Set an additional border to the camera viewport as a buffer before the object gets destroyed.
* Set the additional border outside the camera area.
*
* If the object goes beyond the camera area and this border, it will be deleted (unless it was
* never seen inside the camera area and this border before, in which case it will be deleted
* according to the grace distance).
* @param val Border in pixels.
*/
setExtraBorder(val: number): void {
@@ -59,12 +93,36 @@ namespace gdjs {
}
/**
* Get the additional border of the camera viewport buffer which triggers the destruction of an object.
* Get the additional border outside the camera area.
* @return The additional border around the camera viewport in pixels
*/
getExtraBorder(): number {
return this._extraBorder;
}
/**
* Change the grace distance before an object is deleted if it's outside the camera area
* and was never seen inside the camera area. Typically useful to avoid objects being deleted
* before they are visible when they spawn.
*/
setUnseenGraceDistance(val: number): void {
this._unseenGraceDistance = val;
}
/**
* Get the grace distance before an object is deleted if it's outside the camera area
* and was never seen inside the camera area.
*/
getUnseenGraceDistance(): number {
return this._unseenGraceDistance;
}
/**
* Check if this object has been visible on screen (precisely: inside the camera area *including* the extra border).
*/
hasBeenOnScreen(): boolean {
return this._hasBeenOnScreen;
}
}
gdjs.registerBehavior(
'DestroyOutsideBehavior::DestroyOutside',

View File

@@ -29,6 +29,19 @@ describe('gdjs.DraggableRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -87,16 +87,21 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): LightNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): LightNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
rad: this.getRadius(),
col: this.getColor(),
};
}
updateFromNetworkSyncData(networkSyncData: LightNetworkSyncData): void {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: LightNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.rad !== undefined) {
this.setRadius(networkSyncData.rad);

View File

@@ -35,6 +35,19 @@ describe('gdjs.LinksManager', function () {
stopSoundsOnStartup: false,
title: '',
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -729,7 +729,9 @@ namespace gdjs {
behavior.playerNumber = ownerPlayerNumber;
}
instance.updateFromNetworkSyncData(messageData);
instance.updateFromNetworkSyncData(messageData, {
clearInputs: false,
});
setLastClockReceivedForInstanceOnScene({
sceneNetworkId,
@@ -1737,7 +1739,7 @@ namespace gdjs {
return;
}
runtimeScene.updateFromNetworkSyncData(messageData);
runtimeScene.updateFromNetworkSyncData(messageData, {});
} else {
// If the game is not ready to receive game update messages, we need to save the data for later use.
// This can happen when joining a game that is already running.
@@ -1890,7 +1892,7 @@ namespace gdjs {
const messageData = message.getData();
const messageSender = message.getSender();
if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
runtimeScene.getGame().updateFromNetworkSyncData(messageData, {});
} else {
// If the game is not ready to receive game update messages, we need to save the data for later use.
// This can happen when joining a game that is already running.
@@ -1918,7 +1920,7 @@ namespace gdjs {
// Reapply the game saved updates.
lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {
debugLogger.info(`Reapplying saved update of game.`);
runtimeScene.getGame().updateFromNetworkSyncData(messageData);
runtimeScene.getGame().updateFromNetworkSyncData(messageData, {});
});
// Game updates are always applied properly, so we can clear them.
lastReceivedGameSyncDataUpdates.clear();
@@ -1937,7 +1939,7 @@ namespace gdjs {
debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);
runtimeScene.updateFromNetworkSyncData(messageData);
runtimeScene.updateFromNetworkSyncData(messageData, {});
// We only remove the message if it was successfully applied, so it can be reapplied later,
// in case we were not on the right scene.
lastReceivedSceneSyncDataUpdates.remove(messageData);

View File

@@ -278,7 +278,7 @@ namespace gdjs {
const instanceNetworkId = this._getOrCreateInstanceNetworkId();
const objectName = this.owner.getName();
const objectNetworkSyncData = this.owner.getNetworkSyncData();
const objectNetworkSyncData = this.owner.getNetworkSyncData({});
// this._logToConsoleWithThrottle(
// `Synchronizing object ${this.owner.getName()} (instance ${
@@ -448,7 +448,7 @@ namespace gdjs {
objectOwner: this.playerNumber,
objectName,
instanceNetworkId,
objectNetworkSyncData: this.owner.getNetworkSyncData(),
objectNetworkSyncData: this.owner.getNetworkSyncData({}),
sceneNetworkId,
});
this._sendDataToPeersWithIncreasedClock(
@@ -598,7 +598,7 @@ namespace gdjs {
debugLogger.info(
'Sending update message to move the object immediately.'
);
const objectNetworkSyncData = this.owner.getNetworkSyncData();
const objectNetworkSyncData = this.owner.getNetworkSyncData({});
const {
messageName: updateMessageName,
messageData: updateMessageData,

View File

@@ -123,18 +123,21 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): PanelSpriteNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): PanelSpriteNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
op: this.getOpacity(),
color: this.getColor(),
};
}
updateFromNetworkSyncData(
networkSyncData: PanelSpriteNetworkSyncData
networkSyncData: PanelSpriteNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
// Texture is not synchronized, see if this is asked or not.

View File

@@ -18,12 +18,15 @@ namespace gdjs {
renderer: PIXI.Container;
emitter: PIXI.particles.Emitter;
started: boolean = false;
helperGraphics: PIXI.Graphics | null = null;
runtimeObject: gdjs.ParticleEmitterObject;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
runtimeObject: gdjs.RuntimeObject,
runtimeObject: gdjs.ParticleEmitterObject,
objectData: any
) {
this.runtimeObject = runtimeObject;
const pixiRenderer = instanceContainer
.getGame()
.getRenderer()
@@ -223,6 +226,44 @@ namespace gdjs {
if (!this.started && wasEmitting) {
this.started = true;
}
if (this.helperGraphics) {
this.helperGraphics.clear();
this.helperGraphics.position.x = this.runtimeObject.getX();
this.helperGraphics.position.y = this.runtimeObject.getY();
const emitterAngle = gdjs.toRad(this.runtimeObject.getAngle());
const sprayConeAngle = gdjs.toRad(
this.runtimeObject.getConeSprayAngle()
);
const line1Angle = emitterAngle - sprayConeAngle / 2;
const line2Angle = emitterAngle + sprayConeAngle / 2;
const length = 64;
this.helperGraphics.beginFill(0, 0);
this.helperGraphics.lineStyle(
3,
this.runtimeObject.getParticleColorEnd(),
1
);
this.helperGraphics.moveTo(0, 0);
this.helperGraphics.lineTo(
Math.cos(line1Angle) * length,
Math.sin(line1Angle) * length
);
this.helperGraphics.moveTo(0, 0);
this.helperGraphics.lineTo(
Math.cos(line2Angle) * length,
Math.sin(line2Angle) * length
);
this.helperGraphics.endFill();
this.helperGraphics.lineStyle(0, 0x000000, 1);
this.helperGraphics.beginFill(
this.runtimeObject.getParticleColorStart()
);
this.helperGraphics.drawCircle(0, 0, 8);
this.helperGraphics.endFill();
}
}
setPosition(x: number, y: number): void {
@@ -443,6 +484,17 @@ namespace gdjs {
}
private static readonly frequencyMinimumValue = 0.0001;
setHelperVisible(visible: boolean) {
if (visible && !this.helperGraphics) {
this.helperGraphics = new PIXI.Graphics();
this.renderer.addChild(this.helperGraphics);
} else if (!visible && this.helperGraphics) {
this.helperGraphics.removeFromParent();
this.helperGraphics.destroy();
this.helperGraphics = null;
}
}
}
// @ts-ignore - Register the class to let the engine use it.

View File

@@ -174,6 +174,10 @@ namespace gdjs {
this,
particleObjectData
);
if (instanceContainer.getGame().isInGameEdition()) {
// TODO Disable the particles rendering
this._renderer.setHelperVisible(true);
}
this.angleA = particleObjectData.emitterAngleA;
this.angleB = particleObjectData.emitterAngleB;
this.forceMin = particleObjectData.emitterForceMin;
@@ -370,9 +374,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): ParticleEmitterObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): ParticleEmitterObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
prms: this.particleRotationMinSpeed,
prmx: this.particleRotationMaxSpeed,
mpc: this.maxParticlesCount,
@@ -399,9 +405,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
syncData: ParticleEmitterObjectNetworkSyncData
syncData: ParticleEmitterObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData);
super.updateFromNetworkSyncData(syncData, options);
if (syncData.x !== undefined) {
this.setX(syncData.x);
}
@@ -799,6 +806,14 @@ namespace gdjs {
}
}
getParticleColorStart(): number {
return this.color1;
}
getParticleColorEnd(): number {
return this.color2;
}
getParticleRed1(): number {
return gdjs.hexNumberToRGBArray(this.color1)[0];
}

View File

@@ -6,7 +6,7 @@ namespace gdjs {
const logger = new gdjs.Logger('Pathfinding behavior');
interface PathfindingNetworkSyncDataType {
// Syncing the path should be enough to have a good prediction.
// Syncing the path and its position on it should be enough to have a good prediction.
path: FloatPoint[];
pf: boolean;
sp: number;
@@ -15,6 +15,7 @@ namespace gdjs {
tss: number;
re: boolean;
ma: number;
dos: number;
}
export interface PathfindingNetworkSyncData extends BehaviorNetworkSyncData {
@@ -133,9 +134,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): PathfindingNetworkSyncData {
getNetworkSyncData(
options: GetNetworkSyncDataOptions
): PathfindingNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
path: this._path,
pf: this._pathFound,
@@ -145,14 +148,16 @@ namespace gdjs {
tss: this._totalSegmentDistance,
re: this._reachedEnd,
ma: this._movementAngle,
dos: this._distanceOnSegment,
},
};
}
updateFromNetworkSyncData(
networkSyncData: PathfindingNetworkSyncData
networkSyncData: PathfindingNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.path !== undefined) {
this._path = behaviorSpecificProps.path;
@@ -181,6 +186,9 @@ namespace gdjs {
if (behaviorSpecificProps.ma !== undefined) {
this._movementAngle = behaviorSpecificProps.ma;
}
if (behaviorSpecificProps.dos !== undefined) {
this._distanceOnSegment = behaviorSpecificProps.dos;
}
}
setCellWidth(width: float): void {

View File

@@ -36,6 +36,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -39,6 +39,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -41,6 +41,19 @@ describe('gdjs.PathfindingRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -623,10 +623,46 @@ Zv();a.b2Manifold.e_faceA=$v();a.b2Manifold.e_faceB=aw();a.b2_staticBody=bw();a.
})();
gdjs.registerAsynchronouslyLoadingLibraryPromise(initializeBox2D({locateFile: function(path, prefix) {
return location.protocol === 'file:' ?
// This is needed to run on Electron.
prefix + "Extensions/Physics2Behavior/" + path :
prefix + path;
// Path should always be "Box2D_v2.3.1_min.wasm.wasm" (and if it's not, we should probably hardcode it).
if (path !== 'Box2D_v2.3.1_min.wasm.wasm') {
console.warn("'path' argument sent to locateFile in Box2D_v2.3.1_min.wasm.js is not the expected string 'Box2D_v2.3.1_min.wasm.wasm'. Loading may fail.")
}
// Prefix is typically:
// Games ("exported", standalone game):
// - Web game: "https://games.gdevelop-app.com/[...]/Extensions/Physics2Behavior/"
// - Cordova Android: "https://localhost/Extensions/Physics2Behavior/".
// - Cordova iOS: "ionic://localhost/Extensions/Physics2Behavior/".
// - Electron macOS: "/private/var/[...]/Contents/Resources/app.asar/app/" (notice the missing folder).
// - Electron Windows: "C:\Users\[...]\AppData\Local\[...]\resources\app.asar\app/" (notice the missing folder).
// Preview (in the editor):
// - Web app preview (dev editor): "http://localhost:5002/Runtime/Extensions/Physics2Behavior/"
// - Web app preview (production editor): "https://resources.gdevelop-app.com/[...]/Runtime/Extensions/Physics2Behavior/"
// - Electron app preview (dev editor): "/var/[...]/preview/" (notice the missing folder).
// - Electron app preview (production editor): "/var/[...]/preview/" (notice the missing folder).
// In-game editor:
// - Web app (dev editor): "http://localhost:5002/Runtime/Extensions/Physics2Behavior/"
// - Web app (production editor): "https://resources.gdevelop-app.com/[...]/Runtime/Extensions/Physics2Behavior/"
// - Electron app (dev editor): "file:///var/[...]/in-game-editor-preview/Extensions/Physics2Behavior/"
// - Electron app (production editor): "file:///var/[...]/in-game-editor-preview/Extensions/Physics2Behavior/"
// If the prefix is a full URL, it's a full URL to the folder containing this JS file.
// Sill consider the case where the folder could have been missing.
let url;
if (prefix.startsWith('http:') || prefix.startsWith('https:')) {
url = prefix.endsWith('Extensions/Physics2Behavior/') ?
prefix + path :
prefix + 'Extensions/Physics2Behavior/' + path;
} else {
// Electron or Cordova iOS will fall in this case.
// We can't use this simple solution for http/https because
// on the web-app, the runtime is not necessarily hosted
// on the same domain as where the game generated files are served (so "prefix" is needed).
url = "Extensions/Physics2Behavior/" + path;
}
console.info(`Box2D wasm file is being loaded from path "${path}" with prefix "${prefix}". Resolved URL: "${url}".`);
return url;
}}).then(box2d => {
window.Box2D = box2d;
}));

View File

@@ -499,7 +499,9 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): Physics2NetworkSyncData {
getNetworkSyncData(
options: GetNetworkSyncDataOptions
): Physics2NetworkSyncData {
const bodyProps = this._body
? {
tpx: this._body.GetTransform().get_p().get_x(),
@@ -520,7 +522,7 @@ namespace gdjs {
aw: undefined,
};
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
...bodyProps,
layers: this.layers,
@@ -529,45 +531,13 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: Physics2NetworkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: Physics2NetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (
behaviorSpecificProps.tpx !== undefined &&
behaviorSpecificProps.tpy !== undefined &&
behaviorSpecificProps.tqa !== undefined
) {
if (this._body) {
this._body.SetTransform(
this.b2Vec2(behaviorSpecificProps.tpx, behaviorSpecificProps.tpy),
behaviorSpecificProps.tqa
);
}
}
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined
) {
if (this._body) {
this._body.SetLinearVelocity(
this.b2Vec2(behaviorSpecificProps.lvx, behaviorSpecificProps.lvy)
);
}
}
if (behaviorSpecificProps.av !== undefined) {
if (this._body) {
this._body.SetAngularVelocity(behaviorSpecificProps.av);
}
}
if (behaviorSpecificProps.aw !== undefined) {
if (this._body) {
this._body.SetAwake(behaviorSpecificProps.aw);
}
}
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
@@ -576,6 +546,38 @@ namespace gdjs {
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
}
this.updateBodyFromObject();
if (!this._body) return;
if (
behaviorSpecificProps.tpx !== undefined &&
behaviorSpecificProps.tpy !== undefined &&
behaviorSpecificProps.tqa !== undefined
) {
this._body.SetTransform(
this.b2Vec2(behaviorSpecificProps.tpx, behaviorSpecificProps.tpy),
behaviorSpecificProps.tqa
);
}
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined
) {
this._body.SetLinearVelocity(
this.b2Vec2(behaviorSpecificProps.lvx, behaviorSpecificProps.lvy)
);
}
if (behaviorSpecificProps.av !== undefined) {
this._body.SetAngularVelocity(behaviorSpecificProps.av);
}
if (behaviorSpecificProps.aw !== undefined) {
this._body.SetAwake(behaviorSpecificProps.aw);
}
}
onDeActivate() {

View File

@@ -495,7 +495,9 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): Physics3DNetworkSyncData {
override getNetworkSyncData(
options: GetNetworkSyncDataOptions
): Physics3DNetworkSyncData {
let bodyProps;
if (this._body) {
const position = this._body.GetPosition();
@@ -537,7 +539,7 @@ namespace gdjs {
};
}
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
...bodyProps,
layers: this.layers,
@@ -547,27 +549,40 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: Physics3DNetworkSyncData
networkSyncData: Physics3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
}
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
}
this._needToRecreateShape = true;
this._needToRecreateBody = true;
this.updateBodyFromObject();
if (!this._body) return;
if (
behaviorSpecificProps.px !== undefined &&
behaviorSpecificProps.py !== undefined &&
behaviorSpecificProps.pz !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetPosition(
this._body.GetID(),
this.getRVec3(
behaviorSpecificProps.px,
behaviorSpecificProps.py,
behaviorSpecificProps.pz
),
Jolt.EActivation_DontActivate
);
}
this._sharedData.bodyInterface.SetPosition(
this._body.GetID(),
this.getRVec3(
behaviorSpecificProps.px,
behaviorSpecificProps.py,
behaviorSpecificProps.pz
),
Jolt.EActivation_DontActivate
);
}
if (
behaviorSpecificProps.rx !== undefined &&
@@ -575,56 +590,44 @@ namespace gdjs {
behaviorSpecificProps.rz !== undefined &&
behaviorSpecificProps.rw !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetRotation(
this._body.GetID(),
this.getQuat(
behaviorSpecificProps.rx,
behaviorSpecificProps.ry,
behaviorSpecificProps.rz,
behaviorSpecificProps.rw
),
Jolt.EActivation_DontActivate
);
}
this._sharedData.bodyInterface.SetRotation(
this._body.GetID(),
this.getQuat(
behaviorSpecificProps.rx,
behaviorSpecificProps.ry,
behaviorSpecificProps.rz,
behaviorSpecificProps.rw
),
Jolt.EActivation_DontActivate
);
}
if (
behaviorSpecificProps.lvx !== undefined &&
behaviorSpecificProps.lvy !== undefined &&
behaviorSpecificProps.lvz !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetLinearVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.lvx,
behaviorSpecificProps.lvy,
behaviorSpecificProps.lvz
)
);
}
this._sharedData.bodyInterface.SetLinearVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.lvx,
behaviorSpecificProps.lvy,
behaviorSpecificProps.lvz
)
);
}
if (
behaviorSpecificProps.avx !== undefined &&
behaviorSpecificProps.avy !== undefined &&
behaviorSpecificProps.avz !== undefined
) {
if (this._body) {
this._sharedData.bodyInterface.SetAngularVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.avx,
behaviorSpecificProps.avy,
behaviorSpecificProps.avz
)
);
}
}
if (behaviorSpecificProps.layers !== undefined) {
this.layers = behaviorSpecificProps.layers;
}
if (behaviorSpecificProps.masks !== undefined) {
this.masks = behaviorSpecificProps.masks;
this._sharedData.bodyInterface.SetAngularVelocity(
this._body.GetID(),
this.getVec3(
behaviorSpecificProps.avx,
behaviorSpecificProps.avy,
behaviorSpecificProps.avz
)
);
}
}
@@ -921,31 +924,58 @@ namespace gdjs {
this.updateBodyFromObject();
}
recreateBody() {
if (!this._body) {
this._createBody();
return;
}
recreateBody(previousBodyData?: {
linearVelocityX: float;
linearVelocityY: float;
linearVelocityZ: float;
angularVelocityX: float;
angularVelocityY: float;
angularVelocityZ: float;
}) {
const bodyInterface = this._sharedData.bodyInterface;
const linearVelocity = this._body.GetLinearVelocity();
const linearVelocityX = linearVelocity.GetX();
const linearVelocityY = linearVelocity.GetY();
const linearVelocityZ = linearVelocity.GetZ();
const angularVelocity = this._body.GetAngularVelocity();
const angularVelocityX = angularVelocity.GetX();
const angularVelocityY = angularVelocity.GetY();
const angularVelocityZ = angularVelocity.GetZ();
const linearVelocityX = previousBodyData
? previousBodyData.linearVelocityX
: this._body
? this._body.GetLinearVelocity().GetX()
: 0;
const linearVelocityY = previousBodyData
? previousBodyData.linearVelocityY
: this._body
? this._body.GetLinearVelocity().GetY()
: 0;
const linearVelocityZ = previousBodyData
? previousBodyData.linearVelocityZ
: this._body
? this._body.GetLinearVelocity().GetZ()
: 0;
const angularVelocityX = previousBodyData
? previousBodyData.angularVelocityX
: this._body
? this._body.GetAngularVelocity().GetX()
: 0;
const angularVelocityY = previousBodyData
? previousBodyData.angularVelocityY
: this._body
? this._body.GetAngularVelocity().GetY()
: 0;
const angularVelocityZ = previousBodyData
? previousBodyData.angularVelocityZ
: this._body
? this._body.GetAngularVelocity().GetZ()
: 0;
this.bodyUpdater.destroyBody();
this._contactsEndedThisFrame.length = 0;
this._contactsStartedThisFrame.length = 0;
this._currentContacts.length = 0;
if (this._body) {
this.bodyUpdater.destroyBody();
this._contactsEndedThisFrame.length = 0;
this._contactsStartedThisFrame.length = 0;
this._currentContacts.length = 0;
}
this._createBody();
if (!this._body) {
return;
}
const bodyID = this._body.GetID();
bodyInterface.SetLinearVelocity(
bodyID,

View File

@@ -12,6 +12,7 @@ namespace gdjs {
etm: float;
esm: float;
ei: float;
es: float;
}
export interface PhysicsCar3DNetworkSyncData extends BehaviorNetworkSyncData {
@@ -96,7 +97,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
private _dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
@@ -168,12 +169,37 @@ namespace gdjs {
this._isHookedToPhysicsStep = true;
}
// Destroy the body before switching the bodyUpdater,
// to ensure the body of the previous bodyUpdater is not left alive.
// (would be a memory leak and would create a phantom body in the physics world)
// But transfer the linear and angular velocity to the new body,
// so the body doesn't stop when it is recreated.
let previousBodyData = {
linearVelocityX: 0,
linearVelocityY: 0,
linearVelocityZ: 0,
angularVelocityX: 0,
angularVelocityY: 0,
angularVelocityZ: 0,
};
if (behavior._body) {
const linearVelocity = behavior._body.GetLinearVelocity();
previousBodyData.linearVelocityX = linearVelocity.GetX();
previousBodyData.linearVelocityY = linearVelocity.GetY();
previousBodyData.linearVelocityZ = linearVelocity.GetZ();
const angularVelocity = behavior._body.GetAngularVelocity();
previousBodyData.angularVelocityX = angularVelocity.GetX();
previousBodyData.angularVelocityY = angularVelocity.GetY();
previousBodyData.angularVelocityZ = angularVelocity.GetZ();
behavior.bodyUpdater.destroyBody();
}
behavior.bodyUpdater =
new gdjs.PhysicsCar3DRuntimeBehavior.VehicleBodyUpdater(
this,
behavior.bodyUpdater
);
behavior.recreateBody();
behavior.recreateBody(previousBodyData);
return this._physics3D;
}
@@ -273,13 +299,15 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): PhysicsCar3DNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): PhysicsCar3DNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
props: {
lek: this._wasLeftKeyPressed,
rik: this._wasRightKeyPressed,
@@ -291,14 +319,16 @@ namespace gdjs {
etm: this._engineTorqueMax,
esm: this._engineSpeedMax,
ei: this._engineInertia,
es: this.getEngineSpeed(),
},
};
}
override updateFromNetworkSyncData(
networkSyncData: PhysicsCar3DNetworkSyncData
networkSyncData: PhysicsCar3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
this._hasPressedForwardKey = behaviorSpecificProps.upk;
@@ -311,9 +341,15 @@ namespace gdjs {
this._engineTorqueMax = behaviorSpecificProps.etm;
this._engineSpeedMax = behaviorSpecificProps.esm;
this._engineInertia = behaviorSpecificProps.ei;
if (this._vehicleController) {
this._vehicleController
.GetEngine()
.SetCurrentRPM(behaviorSpecificProps.es);
}
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// When the object is synchronized from the network, the inputs must not be cleared,
// except if asked specifically.
this._clearInputsBetweenFrames = !!options.clearInputs;
}
_getPhysicsPosition(result: Jolt.RVec3): Jolt.RVec3 {
@@ -490,7 +526,7 @@ namespace gdjs {
this._previousAcceleratorStickForce = this._acceleratorStickForce;
this._previousSteeringStickForce = this._steeringStickForce;
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
this._hasPressedForwardKey = false;
this._hasPressedBackwardKey = false;
this._hasPressedRightKey = false;

View File

@@ -2,11 +2,24 @@
namespace gdjs {
interface PhysicsCharacter3DNetworkSyncDataType {
sma: float;
shm: float;
grav: float;
mfs: float;
facc: float;
fdec: float;
fsm: float;
sacc: float;
sdec: float;
ssm: float;
jumpspeed: float;
jumpsustime: float;
sbpa: boolean;
fwa: float;
fws: float;
sws: float;
fs: float;
js: float;
cfs: float;
cjs: float;
cj: boolean;
lek: boolean;
rik: boolean;
@@ -102,7 +115,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
private _dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
/**
* A very small value compare to 1 pixel, yet very huge compare to rounding errors.
@@ -207,10 +220,35 @@ namespace gdjs {
this._isHookedToPhysicsStep = true;
}
// Destroy the body before switching the bodyUpdater,
// to ensure the body of the previous bodyUpdater is not left alive.
// (would be a memory leak and would create a phantom body in the physics world)
// But transfer the linear and angular velocity to the new body,
// so the body doesn't stop when it is recreated.
let previousBodyData = {
linearVelocityX: 0,
linearVelocityY: 0,
linearVelocityZ: 0,
angularVelocityX: 0,
angularVelocityY: 0,
angularVelocityZ: 0,
};
if (behavior._body) {
const linearVelocity = behavior._body.GetLinearVelocity();
previousBodyData.linearVelocityX = linearVelocity.GetX();
previousBodyData.linearVelocityY = linearVelocity.GetY();
previousBodyData.linearVelocityZ = linearVelocity.GetZ();
const angularVelocity = behavior._body.GetAngularVelocity();
previousBodyData.angularVelocityX = angularVelocity.GetX();
previousBodyData.angularVelocityY = angularVelocity.GetY();
previousBodyData.angularVelocityZ = angularVelocity.GetZ();
behavior.bodyUpdater.destroyBody();
}
behavior.bodyUpdater =
new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterBodyUpdater(this);
behavior.collisionChecker = this.collisionChecker;
behavior.recreateBody();
behavior.recreateBody(previousBodyData);
// Always begin in the direction of the object.
this._forwardAngle = this.owner.getAngle();
@@ -277,19 +315,34 @@ namespace gdjs {
return true;
}
override getNetworkSyncData(): PhysicsCharacter3DNetworkSyncData {
override getNetworkSyncData(
options: GetNetworkSyncDataOptions
): PhysicsCharacter3DNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
sma: this._slopeMaxAngle,
shm: this._stairHeightMax,
grav: this._gravity,
mfs: this._maxFallingSpeed,
facc: this._forwardAcceleration,
fdec: this._forwardDeceleration,
fsm: this._forwardSpeedMax,
sacc: this._sidewaysAcceleration,
sdec: this._sidewaysDeceleration,
ssm: this._sidewaysSpeedMax,
jumpspeed: this._jumpSpeed,
jumpsustime: this._jumpSustainTime,
fwa: this._forwardAngle,
sbpa: this._shouldBindObjectAndForwardAngle,
fws: this._currentForwardSpeed,
sws: this._currentSidewaysSpeed,
fs: this._currentFallSpeed,
js: this._currentJumpSpeed,
cfs: this._currentFallSpeed,
cjs: this._currentJumpSpeed,
cj: this._canJump,
lek: this._wasLeftKeyPressed,
rik: this._wasRightKeyPressed,
@@ -306,16 +359,30 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: PhysicsCharacter3DNetworkSyncData
networkSyncData: PhysicsCharacter3DNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
this._slopeMaxAngle = behaviorSpecificProps.sma;
this._stairHeightMax = behaviorSpecificProps.shm;
this._gravity = behaviorSpecificProps.grav;
this._maxFallingSpeed = behaviorSpecificProps.mfs;
this._forwardAcceleration = behaviorSpecificProps.facc;
this._forwardDeceleration = behaviorSpecificProps.fdec;
this._forwardSpeedMax = behaviorSpecificProps.fsm;
this._sidewaysAcceleration = behaviorSpecificProps.sacc;
this._sidewaysDeceleration = behaviorSpecificProps.sdec;
this._sidewaysSpeedMax = behaviorSpecificProps.ssm;
this._jumpSpeed = behaviorSpecificProps.jumpspeed;
this._jumpSustainTime = behaviorSpecificProps.jumpsustime;
this._forwardAngle = behaviorSpecificProps.fwa;
this._shouldBindObjectAndForwardAngle = behaviorSpecificProps.sbpa;
this._currentForwardSpeed = behaviorSpecificProps.fws;
this._currentSidewaysSpeed = behaviorSpecificProps.sws;
this._currentFallSpeed = behaviorSpecificProps.fs;
this._currentJumpSpeed = behaviorSpecificProps.js;
this._currentFallSpeed = behaviorSpecificProps.cfs;
this._currentJumpSpeed = behaviorSpecificProps.cjs;
this._canJump = behaviorSpecificProps.cj;
this._hasPressedForwardKey = behaviorSpecificProps.upk;
this._hasPressedBackwardKey = behaviorSpecificProps.dok;
@@ -328,8 +395,8 @@ namespace gdjs {
this._timeSinceCurrentJumpStart = behaviorSpecificProps.tscjs;
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// Clear user inputs between frames only if requested.
this._clearInputsBetweenFrames = !!options.clearInputs;
}
_getPhysicsPosition(result: Jolt.RVec3): Jolt.RVec3 {
@@ -650,7 +717,7 @@ namespace gdjs {
this._wasJumpKeyPressed = this._hasPressedJumpKey;
this._wasStickUsed = this._hasUsedStick;
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
this._hasPressedForwardKey = false;
this._hasPressedBackwardKey = false;
this._hasPressedRightKey = false;

View File

@@ -147,7 +147,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
_dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
// This is useful when the object is synchronized over the network,
// object is controlled by the network and we want to ensure the current player
// cannot control it.
@@ -227,14 +227,16 @@ namespace gdjs {
this._state = this._falling;
}
getNetworkSyncData(): PlatformerObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): PlatformerObjectNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
this._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
props: {
cs: this._currentSpeed,
@@ -263,11 +265,52 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: PlatformerObjectNetworkSyncData
networkSyncData: PlatformerObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
switch (behaviorSpecificProps.sn) {
case 'Falling':
if (behaviorSpecificProps.sn !== this._state.toString()) {
this._setFalling();
}
this._falling.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
case 'OnFloor':
// Let it handle automatically as we don't know which platform to land on.
// @ts-ignore - we assume it's OnFloorStateNetworkSyncData
this._onFloor.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
case 'Jumping':
if (behaviorSpecificProps.sn !== this._state.toString()) {
this._setJumping();
}
// @ts-ignore - we assume it's JumpingStateNetworkSyncData
this._jumping.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
case 'GrabbingPlatform':
// Let it handle automatically as we don't know which platform to grab.
this._grabbingPlatform.updateFromNetworkSyncData(
// @ts-ignore - we assume it's GrabbingPlatformStateNetworkSyncData
behaviorSpecificProps.ssd
);
break;
case 'OnLadder':
if (behaviorSpecificProps.sn !== this._state.toString()) {
this._setOnLadder();
}
this._onLadder.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
break;
default:
console.error(
'Unknown state name: ' + behaviorSpecificProps.sn + '.'
);
break;
}
if (behaviorSpecificProps.cs !== this._currentSpeed) {
this._currentSpeed = behaviorSpecificProps.cs;
}
@@ -317,39 +360,10 @@ namespace gdjs {
this._jumpKeyHeldSinceJumpStart = behaviorSpecificProps.jkhsjs;
}
if (behaviorSpecificProps.sn !== this._state.toString()) {
switch (behaviorSpecificProps.sn) {
case 'Falling':
this._setFalling();
break;
case 'OnFloor':
// Let it handle automatically as we don't know which platform to land on.
break;
case 'Jumping':
this._setJumping();
break;
case 'GrabbingPlatform':
// Let it handle automatically as we don't know which platform to grab.
break;
case 'OnLadder':
this._setOnLadder();
break;
default:
console.error(
'Unknown state name: ' + behaviorSpecificProps.sn + '.'
);
break;
}
}
if (behaviorSpecificProps.sn === this._state.toString()) {
this._state.updateFromNetworkSyncData(behaviorSpecificProps.ssd);
}
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// Clear user inputs between frames only if requested.
this._clearInputsBetweenFrames = !!options.clearInputs;
// And we are not using the default controls.
this._ignoreDefaultControlsAsSyncedByNetwork = true;
this._ignoreDefaultControlsAsSyncedByNetwork = !options.keepControl;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -545,8 +559,9 @@ namespace gdjs {
this._wasJumpKeyPressed = this._jumpKey;
this._wasReleasePlatformKeyPressed = this._releasePlatformKey;
this._wasReleaseLadderKeyPressed = this._releaseLadderKey;
//4) Do not forget to reset pressed keys
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
// Reset the keys only if the inputs are not supposed to survive between frames.
// (Most of the time, except if this object is synchronized by an external source)
this._leftKey = false;

View File

@@ -157,6 +157,57 @@ namespace gdjs {
return true;
}
trackChangesAndUpdateManagerIfNeeded() {
if (!this.activated() && this._registeredInManager) {
this._manager.removePlatform(this);
this._registeredInManager = false;
} else {
if (this.activated() && !this._registeredInManager) {
this._manager.addPlatform(this);
this._registeredInManager = true;
}
}
if (
this._oldX !== this.owner.getX() ||
this._oldY !== this.owner.getY() ||
this._oldWidth !== this.owner.getWidth() ||
this._oldHeight !== this.owner.getHeight() ||
this._oldAngle !== this.owner.getAngle()
) {
if (this._registeredInManager) {
this._manager.removePlatform(this);
this._manager.addPlatform(this);
}
this._oldX = this.owner.getX();
this._oldY = this.owner.getY();
this._oldWidth = this.owner.getWidth();
this._oldHeight = this.owner.getHeight();
this._oldAngle = this.owner.getAngle();
}
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BehaviorNetworkSyncData {
return super.getNetworkSyncData(syncOptions);
}
updateFromNetworkSyncData(
networkSyncData: BehaviorNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
this.trackChangesAndUpdateManagerIfNeeded();
}
onCreated(): void {
// Register it right away if activated,
// so it can be used by platformer objects in that same frame.
this.trackChangesAndUpdateManagerIfNeeded();
}
onDestroy() {
if (this._manager && this._registeredInManager) {
this._manager.removePlatform(this);
@@ -175,34 +226,7 @@ namespace gdjs {
}*/
//Make sure the platform is or is not in the platforms manager.
if (!this.activated() && this._registeredInManager) {
this._manager.removePlatform(this);
this._registeredInManager = false;
} else {
if (this.activated() && !this._registeredInManager) {
this._manager.addPlatform(this);
this._registeredInManager = true;
}
}
//Track changes in size or position
if (
this._oldX !== this.owner.getX() ||
this._oldY !== this.owner.getY() ||
this._oldWidth !== this.owner.getWidth() ||
this._oldHeight !== this.owner.getHeight() ||
this._oldAngle !== this.owner.getAngle()
) {
if (this._registeredInManager) {
this._manager.removePlatform(this);
this._manager.addPlatform(this);
}
this._oldX = this.owner.getX();
this._oldY = this.owner.getY();
this._oldWidth = this.owner.getWidth();
this._oldHeight = this.owner.getHeight();
this._oldAngle = this.owner.getAngle();
}
this.trackChangesAndUpdateManagerIfNeeded();
}
doStepPostEvents(instanceContainer: gdjs.RuntimeInstanceContainer) {}

View File

@@ -20,6 +20,8 @@ namespace gdjs {
_antialiasingFilter: null | PIXI.Filter = null;
_placeholder: PIXI.Sprite | null = null;
private static readonly _positionForTransformation: PIXI.IPointData = {
x: 0,
y: 0,
@@ -405,6 +407,25 @@ namespace gdjs {
updatePreRender(): void {
this.updatePositionIfNeeded();
const game = this._object.getRuntimeScene().getGame();
if (
game.isInGameEdition() &&
this._graphics.geometry.graphicsData.length === 0
) {
if (!this._placeholder) {
console.log(game.getGameData().resources.resources);
const texture = game
.getImageManager()
.getPIXITexture('InGameEditor-ShapePainterIcon');
this._placeholder = new PIXI.Sprite(texture);
}
this._graphics.addChild(this._placeholder);
} else if (this._placeholder) {
this._placeholder.removeFromParent();
this._placeholder.destroy();
this._placeholder = null;
}
}
updatePositionX(): void {

View File

@@ -37,6 +37,24 @@ namespace gdjs {
export type ShapePainterObjectData = ObjectData & ShapePainterObjectDataType;
type ShapePainterNetworkSyncDataType = {
cbf: boolean; // clearBetweenFrames
aa: Antialiasing; // antialiasing
ac: boolean; // absoluteCoordinates
fc: integer; // fillColor
oc: integer; // outlineColor
os: float; // outlineSize
fo: float; // fillOpacity
oo: float; // outlineOpacity
scaleX: number;
scaleY: number;
ifx: boolean; // isFlippedX
ify: boolean; // isFlippedY
};
export type ShapePainterNetworkSyncData = ObjectNetworkSyncData &
ShapePainterNetworkSyncDataType;
/**
* The ShapePainterRuntimeObject allows to draw graphics shapes on screen.
*/
@@ -191,6 +209,70 @@ namespace gdjs {
return true;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): ShapePainterNetworkSyncData {
return {
...super.getNetworkSyncData(syncOptions),
cbf: this._clearBetweenFrames,
aa: this._antialiasing,
ac: this._useAbsoluteCoordinates,
fc: this._fillColor,
oc: this._outlineColor,
os: this._outlineSize,
fo: this._fillOpacity,
oo: this._outlineOpacity,
scaleX: this.getScaleX(),
scaleY: this.getScaleY(),
ifx: this._flippedX,
ify: this._flippedY,
};
}
updateFromNetworkSyncData(
networkSyncData: ShapePainterNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.cbf !== undefined) {
this._clearBetweenFrames = networkSyncData.cbf;
}
if (networkSyncData.aa !== undefined) {
this.setAntialiasing(networkSyncData.aa);
}
if (networkSyncData.ac !== undefined) {
this.setCoordinatesRelative(!networkSyncData.ac);
}
if (networkSyncData.fc !== undefined) {
this._fillColor = networkSyncData.fc;
}
if (networkSyncData.oc !== undefined) {
this._outlineColor = networkSyncData.oc;
}
if (networkSyncData.os !== undefined) {
this.setOutlineSize(networkSyncData.os);
}
if (networkSyncData.fo !== undefined) {
this.setFillOpacity(networkSyncData.fo);
}
if (networkSyncData.oo !== undefined) {
this.setOutlineOpacity(networkSyncData.oo);
}
if (networkSyncData.scaleX !== undefined) {
this.setScaleX(networkSyncData.scaleX);
}
if (networkSyncData.scaleY !== undefined) {
this.setScaleY(networkSyncData.scaleY);
}
if (networkSyncData.ifx !== undefined) {
this.flipX(networkSyncData.ifx);
}
if (networkSyncData.ify !== undefined) {
this.flipY(networkSyncData.ify);
}
}
/**
* Initialize the extra parameters that could be set for an instance.
* @param initialInstanceData The extra parameters

View File

@@ -50,6 +50,19 @@ describe('gdjs.ShapePainterRuntimeObject (using a PixiJS RuntimeGame with assets
instances: [],
variables: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -0,0 +1,190 @@
//@ts-check
/// <reference path="../JsExtensionTypes.d.ts" />
/**
* This is a declaration of an extension for GDevelop 5.
*
* Changes in this file are watched and automatically imported if the editor
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
*
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
* ⚠️ If you make a change and the extension is not loaded, open the developer console
* and search for any errors.
*
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
*/
/** @type {ExtensionModule} */
module.exports = {
createExtension: function (_, gd) {
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'SaveState',
_('Save State (experimental)'),
_('Allows to save and load the full state of a game.'),
'Neyl Mahfouf',
'Open source (MIT License)'
)
.setExtensionHelpPath('/all-features/save-state')
.setCategory('Game mechanic')
.addInstructionOrExpressionGroupMetadata(_('Save State (experimental)'))
.setIcon('res/actions/saveDown.svg');
extension
.addAction(
'SaveGameSnapshotToVariable',
_('Save game to a variable'),
_('Takes a snapshot of the game and save it to a variable.'),
_('Save the game in variable _PARAM1_'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('variable', _('Variable to store the save to'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.saveVariableGameSnapshot');
extension
.addAction(
'SaveGameSnapshotToStorage',
_('Save game to device storage'),
_('Takes a snapshot of the game and save it to device storage.'),
_('Save the game to device storage under key _PARAM1_'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('string', _('Storage key to save to'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.saveStorageGameSnapshot');
extension
.addAction(
'LoadGameSnapshotFromVariable',
_('Load game from variable'),
_('Load game from a variable save snapshot.'),
_('Load the game from variable _PARAM0_'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addParameter('variable', _('Variable to load the game from'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.loadGameFromVariableSnapshot');
extension
.addAction(
'LoadGameSnapshotFromStorage',
_('Load game from device storage'),
_('Load game from device storage save snapshot.'),
_('Load the game from device storage under key _PARAM0_.'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.addParameter('string', _('Storage key to load the game from'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.loadGameFromStorageSnapshot');
extension
.addExpressionAndCondition(
'number',
'TimeSinceLastSave',
_('Time since last save'),
_(
'Time since the last save, in seconds. Returns -1 if no save happened, and a positive number otherwise.'
),
_('Time since the last save'),
'',
'res/actions/saveDown.svg'
)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.getSecondsSinceLastSave')
.setGetter('gdjs.saveState.getSecondsSinceLastSave');
extension
.addExpressionAndCondition(
'number',
'TimeSinceLastLoad',
_('Time since last load'),
_(
'Time since the last load, in seconds. Returns -1 if no load happened, and a positive number otherwise.'
),
_('Time since the last load'),
'',
'res/actions/saveDown.svg'
)
.useStandardParameters('number', gd.ParameterOptions.makeNewOptions())
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.getSecondsSinceLastLoad')
.setGetter('gdjs.saveState.getSecondsSinceLastLoad');
extension
.addCondition(
'SaveJustSucceeded',
_('Save just succeeded'),
_('the save just succeeded'),
_('the save just succeeded'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasSaveJustSucceeded');
extension
.addCondition(
'SaveJustFailed',
_('Save just failed'),
_('the save just failed'),
_('the save just failed'),
_('Save'),
'res/actions/saveDown.svg',
'res/actions/saveDown.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasSaveJustFailed');
extension
.addCondition(
'LoadJustSucceeded',
_('Load just succeeded'),
_('the load just succeeded'),
_('the load just succeeded'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasLoadJustSucceeded');
extension
.addCondition(
'LoadJustFailed',
_('Load just failed'),
_('the load just failed'),
_('the load just failed'),
_('Load'),
'res/actions/saveUp.svg',
'res/actions/saveUp.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/SaveState/savestatetools.js')
.setFunctionName('gdjs.saveState.hasLoadJustFailed');
return extension;
},
runExtensionSanityTests: function (gd, extension) {
return [];
},
};

View File

@@ -0,0 +1,360 @@
namespace gdjs {
const logger = new gdjs.Logger('Save state');
export type LoadRequestOptions = {
loadStorageName?: string;
loadVariable?: gdjs.Variable;
};
export namespace saveState {
export const getIndexedDbDatabaseName = () => {
const gameId = gdjs.projectData.properties.projectUuid;
return `gdevelop-game-${gameId}`;
};
export const getIndexedDbObjectStore = () => {
return `game-saves`;
};
export const getIndexedDbStorageKey = (key: string) => {
return `save-${key}`;
};
const getNetworkSyncOptions: GetNetworkSyncDataOptions = {
syncObjectIdentifiers: true,
syncAllVariables: true,
syncAllBehaviors: true,
syncSceneTimers: true,
syncOnceTriggers: true,
syncSounds: true,
syncTweens: true,
syncLayers: true,
syncAsyncTasks: true,
syncSceneVisualProps: true,
syncFullTileMaps: true,
};
const updateFromNetworkSyncDataOptions: UpdateFromNetworkSyncDataOptions = {
clearSceneStack: true,
preventInitialInstancesCreation: true,
preventSoundsStoppingOnStartup: true,
clearInputs: true,
keepControl: true,
ignoreVariableOwnership: true,
};
let lastSaveTime: number | null = null;
let lastLoadTime: number | null = null;
let saveJustSucceeded: boolean = false;
let saveJustFailed: boolean = false;
let loadJustSucceeded: boolean = false;
let loadJustFailed: boolean = false;
let loadRequestOptions: LoadRequestOptions | null = null;
export const getSecondsSinceLastSave = (): number => {
if (!lastSaveTime) return -1;
return Math.floor((Date.now() - lastSaveTime) / 1000);
};
export const getSecondsSinceLastLoad = (): number => {
if (!lastLoadTime) return -1;
return Math.floor((Date.now() - lastLoadTime) / 1000);
};
export const hasSaveJustSucceeded = () => {
return saveJustSucceeded;
};
export const hasLoadJustSucceeded = () => {
return loadJustSucceeded;
};
export const hasSaveJustFailed = () => {
return saveJustFailed;
};
export const hasLoadJustFailed = () => {
return loadJustFailed;
};
export const markSaveJustSucceeded = () => {
saveJustSucceeded = true;
lastSaveTime = Date.now();
};
export const markLoadJustSucceeded = () => {
loadJustSucceeded = true;
lastLoadTime = Date.now();
};
export const markSaveJustFailed = () => {
saveJustFailed = true;
};
export const markLoadJustFailed = () => {
loadJustFailed = true;
};
// Ensure that the condition "save/load just succeeded/failed" are valid only for one frame.
gdjs.registerRuntimeScenePostEventsCallback(() => {
saveJustSucceeded = false;
saveJustFailed = false;
loadJustSucceeded = false;
loadJustFailed = false;
});
gdjs.registerRuntimeScenePostEventsCallback(
(runtimeScene: gdjs.RuntimeScene) => {
loadGameSnapshotAtTheEndOfFrameIfAny(runtimeScene);
}
);
const getGameSaveState = (runtimeScene: RuntimeScene) => {
const gameSaveState: GameSaveState = {
gameNetworkSyncData: {},
layoutNetworkSyncDatas: [],
};
const gameData = runtimeScene
.getGame()
.getNetworkSyncData(getNetworkSyncOptions);
const scenes = runtimeScene.getGame().getSceneStack().getAllScenes();
gameSaveState.gameNetworkSyncData = gameData || {};
scenes.forEach((scene, index) => {
gameSaveState.layoutNetworkSyncDatas[index] = {
sceneData: {} as LayoutNetworkSyncData,
objectDatas: {},
};
// First collect all object sync data, as they may generate unique
// identifiers like their networkId.
const sceneRuntimeObjects = scene.getAdhocListOfAllInstances();
for (const key in sceneRuntimeObjects) {
if (sceneRuntimeObjects.hasOwnProperty(key)) {
const object = sceneRuntimeObjects[key];
const objectSyncData = object.getNetworkSyncData(
getNetworkSyncOptions
);
gameSaveState.layoutNetworkSyncDatas[index].objectDatas[object.id] =
objectSyncData;
}
}
// Collect all scene data in the end.
const sceneDatas = (scene.getNetworkSyncData(getNetworkSyncOptions) ||
[]) as LayoutNetworkSyncData;
gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneDatas;
});
return gameSaveState;
};
export const saveVariableGameSnapshot = async function (
currentScene: RuntimeScene,
variable: gdjs.Variable
) {
try {
const gameSaveState = getGameSaveState(currentScene);
variable.fromJSObject(gameSaveState);
markSaveJustSucceeded();
} catch (error) {
logger.error('Error saving to variable:', error);
markSaveJustFailed();
}
};
export const saveStorageGameSnapshot = async function (
currentScene: RuntimeScene,
storageKey: string
) {
try {
const gameSaveState = getGameSaveState(currentScene);
await gdjs.indexedDb.saveToIndexedDB(
getIndexedDbDatabaseName(),
getIndexedDbObjectStore(),
getIndexedDbStorageKey(storageKey),
gameSaveState
);
markSaveJustSucceeded();
} catch (error) {
logger.error('Error saving to IndexedDB:', error);
markSaveJustFailed();
}
};
export const loadGameFromVariableSnapshot = async function (
variable: gdjs.Variable
) {
// The information is saved, so that the load can be done
// at the end of the frame,
// and avoid possible conflicts with running events.
loadRequestOptions = {
loadVariable: variable,
};
};
export const loadGameFromStorageSnapshot = async function (
storageName: string
) {
// The information is saved, so that the load can be done
// at the end of the frame,
// and avoid possible conflicts with running events.
loadRequestOptions = {
loadStorageName: storageName,
};
};
const loadGameSnapshotAtTheEndOfFrameIfAny = function (
runtimeScene: RuntimeScene
) {
if (!loadRequestOptions) return;
const optionsToApply = loadRequestOptions;
// Reset it so we don't load it twice.
loadRequestOptions = null;
if (optionsToApply.loadVariable) {
const sceneVariables = runtimeScene.getVariables();
const variablePathInScene =
sceneVariables.getVariablePathInContainerByLoopingThroughAllVariables(
optionsToApply.loadVariable
);
const gameVariables = runtimeScene.getGame().getVariables();
const variablePathIngame =
gameVariables.getVariablePathInContainerByLoopingThroughAllVariables(
optionsToApply.loadVariable
);
const saveState =
optionsToApply.loadVariable.toJSObject() as GameSaveState;
try {
loadGameFromSave(runtimeScene, saveState, {
variableToRehydrate: optionsToApply.loadVariable,
variablePathInScene: variablePathInScene,
variablePathInGame: variablePathIngame,
});
markLoadJustSucceeded();
} catch (error) {
logger.error('Error loading from variable:', error);
markLoadJustFailed();
}
} else if (optionsToApply.loadStorageName) {
gdjs.indexedDb
.loadFromIndexedDB(
getIndexedDbDatabaseName(),
getIndexedDbObjectStore(),
getIndexedDbStorageKey(optionsToApply.loadStorageName)
)
.then((jsonData) => {
const saveState = jsonData as GameSaveState;
loadGameFromSave(runtimeScene, saveState);
markLoadJustSucceeded();
})
.catch((error) => {
logger.error('Error loading from IndexedDB:', error);
markLoadJustFailed();
});
}
};
const loadGameFromSave = (
runtimeScene: RuntimeScene,
saveState: GameSaveState,
saveOptions?: {
variableToRehydrate: gdjs.Variable;
variablePathInScene: string[] | null;
variablePathInGame: string[] | null;
}
): void => {
// Save the content of the save, as it will be erased after the load.
const variableToRehydrateNetworkSyncData = saveOptions
? saveOptions.variableToRehydrate.getNetworkSyncData(
getNetworkSyncOptions
)
: null;
// First update the game, which will update the variables,
// and set the scene stack to update when ready.
const runtimeGame = runtimeScene.getGame();
runtimeGame.updateFromNetworkSyncData(
saveState.gameNetworkSyncData,
updateFromNetworkSyncDataOptions
);
// Apply the scene stack updates, as we are at the end of a frame,
// we can safely do it.
const sceneStack = runtimeGame.getSceneStack();
sceneStack.applyUpdateFromNetworkSyncDataIfAny(
updateFromNetworkSyncDataOptions
);
// Then get all scenes, which we assume will be the expected ones
// after the load has been done, so we can update them,
// and create their objects.
const runtimeScenes = sceneStack.getAllScenes();
runtimeScenes.forEach((scene, index) => {
const layoutSyncData = saveState.layoutNetworkSyncDatas[index];
if (!layoutSyncData) return;
// Create objects first, so they are available for the scene update,
// especially so that they have a networkId defined.
const objectDatas = layoutSyncData.objectDatas;
for (const id in objectDatas) {
const objectNetworkSyncData = objectDatas[id];
const objectName = objectNetworkSyncData.n;
if (!objectName) {
logger.warn('Tried to recreate an object without a name.');
continue;
}
const object = scene.createObject(objectName);
if (object) {
object.updateFromNetworkSyncData(
objectNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
// Update the scene last.
scene.updateFromNetworkSyncData(
layoutSyncData.sceneData,
updateFromNetworkSyncDataOptions
);
});
// Finally, if the save was done in a variable,
// rehydrate the variable where the save was done,
// as it has been erased by the load.
if (saveOptions && variableToRehydrateNetworkSyncData) {
const currentScene = sceneStack.getCurrentScene();
if (!currentScene) return;
const sceneVariables = currentScene.getVariables();
const gameVariables = currentScene.getGame().getVariables();
const { variablePathInScene, variablePathInGame } = saveOptions;
if (variablePathInScene && variablePathInScene.length > 0) {
const variableName =
variablePathInScene[variablePathInScene.length - 1];
const variableInScene =
sceneVariables.getVariableFromPath(variablePathInScene);
if (variableInScene) {
const variableNetworkSyncData: VariableNetworkSyncData = {
name: variableName,
...variableToRehydrateNetworkSyncData,
};
variableInScene.updateFromNetworkSyncData(
variableNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
if (variablePathInGame && variablePathInGame.length > 0) {
const variableName =
variablePathInGame[variablePathInGame.length - 1];
const variableInGame =
gameVariables.getVariableFromPath(variablePathInGame);
if (variableInGame) {
const variableNetworkSyncData: VariableNetworkSyncData = {
name: variableName,
...variableToRehydrateNetworkSyncData,
};
variableInGame.updateFromNetworkSyncData(
variableNetworkSyncData,
updateFromNetworkSyncDataOptions
);
}
}
}
};
}
}

View File

@@ -53,6 +53,8 @@ namespace gdjs {
readonly spineResourceName: string;
static isHitBoxesUpdateDisabled = false;
/**
* @param instanceContainer The container the object belongs to.
* @param objectData The object data used to initialize the object
@@ -74,6 +76,10 @@ namespace gdjs {
this.setAnimationIndex(0);
this._renderer.updateAnimation(0);
if (SpineRuntimeObject.isHitBoxesUpdateDisabled) {
this.hitBoxes.length = 0;
}
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
}
@@ -111,9 +117,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): SpineNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SpineNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
opa: this._opacity,
scaX: this.getScaleX(),
scaY: this.getScaleY(),
@@ -127,8 +135,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(syncData: SpineNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
updateFromNetworkSyncData(
syncData: SpineNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData, options);
if (syncData.opa !== undefined && syncData.opa !== this._opacity) {
this.setOpacity(syncData.opa);
@@ -183,6 +194,14 @@ namespace gdjs {
}
}
updateHitBoxes(): void {
if (SpineRuntimeObject.isHitBoxesUpdateDisabled) {
return;
}
super.updateHitBoxes();
}
extraInitializationFromInitialInstance(
initialInstanceData: InstanceData
): void {

View File

@@ -61,6 +61,19 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme
instances: [],
variables: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -269,9 +269,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TextInputNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TextInputNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
opa: this.getOpacity(),
txt: this.getText(),
frn: this.getFontResourceName(),
@@ -290,8 +292,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(syncData: TextInputNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
updateFromNetworkSyncData(
syncData: TextInputNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData, options);
if (syncData.opa !== undefined) this.setOpacity(syncData.opa);
if (syncData.txt !== undefined) this.setText(syncData.txt);

View File

@@ -52,6 +52,7 @@ namespace gdjs {
c: number[];
scale: number;
ta: string;
vta: string;
wrap: boolean;
wrapw: float;
oena: boolean;
@@ -179,9 +180,6 @@ namespace gdjs {
if (oldContent.text !== newContent.text) {
this.setText(newContent.text);
}
if (oldContent.underlined !== newContent.underlined) {
return false;
}
if (oldContent.textAlignment !== newContent.textAlignment) {
this.setTextAlignment(newContent.textAlignment);
}
@@ -220,12 +218,18 @@ namespace gdjs {
if ((oldContent.lineHeight || 0) !== (newContent.lineHeight || 0)) {
this.setLineHeight(newContent.lineHeight || 0);
}
if (oldContent.underlined !== newContent.underlined) {
return false;
}
return true;
}
override getNetworkSyncData(): TextObjectNetworkSyncData {
override getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TextObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
str: this._str,
o: this.opacity,
cs: this._characterSize,
@@ -236,6 +240,7 @@ namespace gdjs {
c: this._color,
scale: this.getScale(),
ta: this._textAlign,
vta: this._verticalTextAlignment,
wrap: this._wrapping,
wrapw: this._wrappingWidth,
oena: this._isOutlineEnabled,
@@ -253,9 +258,10 @@ namespace gdjs {
}
override updateFromNetworkSyncData(
networkSyncData: TextObjectNetworkSyncData
networkSyncData: TextObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.str !== undefined) {
this.setText(networkSyncData.str);
}
@@ -286,8 +292,8 @@ namespace gdjs {
if (networkSyncData.ta !== undefined) {
this.setTextAlignment(networkSyncData.ta);
}
if (networkSyncData.ta !== undefined) {
this.setVerticalTextAlignment(networkSyncData.ta);
if (networkSyncData.vta !== undefined) {
this.setVerticalTextAlignment(networkSyncData.vta);
}
if (networkSyncData.wrap !== undefined) {
this.setWrapping(networkSyncData.wrap);

View File

@@ -9,7 +9,7 @@ import {
* A tile map model.
*
* Tile map files are parsed into this model by {@link TiledTileMapLoader} or {@link LDtkTileMapLoader}.
* This model is used for rending ({@link TileMapRuntimeObjectPixiRenderer})
* This model is used for rendering ({@link TileMapRuntimeObjectPixiRenderer})
* and hitboxes handling ({@link TransformedCollisionTileMap}).
* This allows to support new file format with only a new parser.
*/
@@ -71,7 +71,7 @@ export declare class EditableTileMap {
tileSetRowCount: number;
}
): EditableTileMap;
toJSObject(): Object;
toJSObject(): EditableTileMapAsJsObject;
/**
* @returns The tile map width in pixels.
*/
@@ -205,7 +205,7 @@ declare abstract class AbstractEditableLayer {
*/
constructor(tileMap: EditableTileMap, id: integer);
setVisible(visible: boolean): void;
toJSObject(): Object;
toJSObject(): EditableTileMapLayerAsJsObject;
/**
* @returns true if the layer is visible.
*/
@@ -284,7 +284,7 @@ export declare class EditableTileMapLayer extends AbstractEditableLayer {
tileMap: EditableTileMap,
isTileIdValid: (tileId: number) => boolean
): EditableTileMapLayer;
toJSObject(): Object;
toJSObject(): EditableTileMapLayerAsJsObject;
/**
* The opacity (between 0-1) of the layer
*/

View File

@@ -2,7 +2,6 @@
namespace gdjs {
export type SimpleTileMapObjectDataType = {
content: {
opacity: number;
atlasImage: string;
rowCount: number;
columnCount: number;
@@ -16,8 +15,7 @@ namespace gdjs {
export type SimpleTileMapNetworkSyncDataType = {
op: number;
ai: string;
// TODO: Support tilemap synchronization. Find an efficient way to send tiles changes.
tm?: TileMapHelper.EditableTileMapAsJsObject;
};
export type SimpleTileMapNetworkSyncData = ObjectNetworkSyncData &
@@ -38,6 +36,7 @@ namespace gdjs {
_opacity: float = 255;
_atlasImage: string;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_tileMap: TileMapHelper.EditableTileMap | null = null;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
readonly _rowCount: number;
readonly _columnCount: number;
@@ -62,7 +61,6 @@ namespace gdjs {
objectData: SimpleTileMapObjectDataType
) {
super(instanceContainer, objectData);
this._opacity = objectData.content.opacity;
this._atlasImage = objectData.content.atlasImage;
this._rowCount = objectData.content.rowCount;
this._columnCount = objectData.content.columnCount;
@@ -87,16 +85,19 @@ namespace gdjs {
instanceContainer
);
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
this._renderer.updatePosition();
this._loadTileMap(
this._initialTileMapAsJsObject,
(tileMap: TileMapHelper.EditableTileMap) => {
this._renderer.updatePosition();
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
this.updateTransformation();
});
this.updateTransformation();
}
);
// *ALWAYS* call `this.onCreated()` at the very end of your object constructor.
this.onCreated();
@@ -139,8 +140,8 @@ namespace gdjs {
);
if (!shouldContinue) return;
if (this._collisionTileMap) {
const tileMap = this._renderer.getTileMap();
if (tileMap) this._collisionTileMap.updateFromTileMap(tileMap);
if (this._tileMap)
this._collisionTileMap.updateFromTileMap(this._tileMap);
}
this._isTileMapDirty = false;
}
@@ -150,9 +151,6 @@ namespace gdjs {
oldObjectData: SimpleTileMapObjectData,
newObjectData: SimpleTileMapObjectData
): boolean {
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
this.setOpacity(newObjectData.content.opacity);
}
if (
oldObjectData.content.atlasImage !== newObjectData.content.atlasImage
) {
@@ -163,27 +161,58 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): SimpleTileMapNetworkSyncData {
return {
...super.getNetworkSyncData(),
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SimpleTileMapNetworkSyncData {
const syncData: SimpleTileMapNetworkSyncData = {
...super.getNetworkSyncData(syncOptions),
op: this._opacity,
ai: this._atlasImage,
};
if (this._tileMap && syncOptions.syncFullTileMaps) {
const currentTileMapAsJsObject = this._tileMap.toJSObject();
syncData.tm = currentTileMapAsJsObject;
}
return syncData;
}
updateFromNetworkSyncData(
networkSyncData: SimpleTileMapNetworkSyncData
networkSyncData: SimpleTileMapNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (
networkSyncData.op !== undefined &&
networkSyncData.op !== this._opacity
) {
this.setOpacity(networkSyncData.op);
}
if (networkSyncData.ai !== undefined) {
// TODO: support changing the atlas texture
if (networkSyncData.tm !== undefined) {
this._loadTileMap(
networkSyncData.tm,
(tileMap: TileMapHelper.EditableTileMap) => {
if (networkSyncData.w !== undefined) {
this.setWidth(networkSyncData.w);
}
if (networkSyncData.h !== undefined) {
this.setHeight(networkSyncData.h);
}
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
}
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
if (this._collisionTileMap) {
// If collision tile map is already defined, only update it.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap =
new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
}
this.updateTransformation();
}
);
}
}
@@ -199,41 +228,45 @@ namespace gdjs {
// 2. Update the renderer so that it updates the tilemap object
// (used for width and position calculations).
this._loadInitialTileMap((tileMap: TileMapHelper.EditableTileMap) => {
// 3. Set custom dimensions & opacity if applicable.
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
if (this._collisionTileMap) {
// If collision tile map is already defined, there's a good chance it means
// extraInitializationFromInitialInstance is called when hot reloading the
// scene so the collision is tile map is updated instead of being re-created.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap = new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
this._loadTileMap(
this._initialTileMapAsJsObject,
(tileMap: TileMapHelper.EditableTileMap) => {
// 3. Set custom dimensions & opacity if applicable.
if (initialInstanceData.customSize) {
this.setWidth(initialInstanceData.width);
this.setHeight(initialInstanceData.height);
}
this.setOpacity(
initialInstanceData.opacity === undefined
? 255
: initialInstanceData.opacity
);
}
this.updateTransformation();
});
// 4. Update position (calculations based on renderer's dimensions).
this._renderer.updatePosition();
if (this._collisionTileMap) {
// If collision tile map is already defined, there's a good chance it means
// extraInitializationFromInitialInstance is called when hot reloading the
// scene so the collision is tile map is updated instead of being re-created.
this._collisionTileMap.updateFromTileMap(tileMap);
} else {
this._collisionTileMap =
new gdjs.TileMap.TransformedCollisionTileMap(
tileMap,
this._hitBoxTag
);
}
this.updateTransformation();
}
);
}
private _loadInitialTileMap(
private _loadTileMap(
tileMapAsJsObject: TileMapHelper.EditableTileMapAsJsObject,
tileMapLoadingCallback: (tileMap: TileMapHelper.EditableTileMap) => void
): void {
if (!this._initialTileMapAsJsObject) return;
if (this._columnCount <= 0 || this._rowCount <= 0) {
console.error(
`Tilemap object ${this.name} is not configured properly.`
@@ -242,7 +275,7 @@ namespace gdjs {
}
this._tileMapManager.getOrLoadSimpleTileMap(
this._initialTileMapAsJsObject,
tileMapAsJsObject,
this.name,
this._tileSize,
this._columnCount,
@@ -286,7 +319,8 @@ namespace gdjs {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.updatePixiTileMap(tileMap, textureCache);
this._tileMap = tileMap;
this._renderer.refreshPixiTileMap(textureCache);
tileMapLoadingCallback(tileMap);
},
(error) => {
@@ -418,11 +452,11 @@ namespace gdjs {
}
override getOriginalWidth(): float {
return this._renderer.getTileMapWidth();
return this.getTileMapWidth();
}
override getOriginalHeight(): float {
return this._renderer.getTileMapHeight();
return this.getTileMapHeight();
}
getScaleX(): float {
@@ -650,7 +684,7 @@ namespace gdjs {
}
getTileAtGridCoordinates(columnIndex: integer, rowIndex: integer): integer {
return this._renderer.getTileId(columnIndex, rowIndex, 0);
return this.getTileId(columnIndex, rowIndex, 0);
}
setTileAtPosition(tileId: number, x: float, y: float) {
@@ -664,11 +698,10 @@ namespace gdjs {
columnIndex: integer,
rowIndex: integer
) {
const tileMap = this._renderer._tileMap;
if (!tileMap) {
if (!this._tileMap) {
return;
}
const layer = tileMap.getTileLayer(this._layerIndex);
const layer = this._tileMap.getTileLayer(this._layerIndex);
if (!layer) {
return;
}
@@ -680,8 +713,8 @@ namespace gdjs {
if (this._collisionTileMap) {
const oldTileDefinition =
oldTileId !== undefined && tileMap.getTileDefinition(oldTileId);
const newTileDefinition = tileMap.getTileDefinition(tileId);
oldTileId !== undefined && this._tileMap.getTileDefinition(oldTileId);
const newTileDefinition = this._tileMap.getTileDefinition(tileId);
const hadFullHitBox =
!!oldTileDefinition &&
oldTileDefinition.hasFullHitBox(this._hitBoxTag);
@@ -717,7 +750,7 @@ namespace gdjs {
rowIndex: integer,
flip: boolean
) {
this._renderer.flipTileOnY(columnIndex, rowIndex, 0, flip);
this.flipTileOnY(columnIndex, rowIndex, 0, flip);
this._isTileMapDirty = true;
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
@@ -728,7 +761,7 @@ namespace gdjs {
rowIndex: integer,
flip: boolean
) {
this._renderer.flipTileOnX(columnIndex, rowIndex, 0, flip);
this.flipTileOnX(columnIndex, rowIndex, 0, flip);
this._isTileMapDirty = true;
// No need to invalidate hit boxes since at the moment, collision mask
// cannot be configured on each tile.
@@ -738,22 +771,22 @@ namespace gdjs {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
return this.isTileFlippedOnX(columnIndex, rowIndex, 0);
}
isTileFlippedOnXAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this._renderer.isTileFlippedOnX(columnIndex, rowIndex, 0);
return this.isTileFlippedOnX(columnIndex, rowIndex, 0);
}
isTileFlippedOnYAtPosition(x: float, y: float) {
const [columnIndex, rowIndex] =
this.getGridCoordinatesFromSceneCoordinates(x, y);
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
return this.isTileFlippedOnY(columnIndex, rowIndex, 0);
}
isTileFlippedOnYAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
return this._renderer.isTileFlippedOnY(columnIndex, rowIndex, 0);
return this.isTileFlippedOnY(columnIndex, rowIndex, 0);
}
removeTileAtPosition(x: float, y: float) {
@@ -763,11 +796,10 @@ namespace gdjs {
}
removeTileAtGridCoordinates(columnIndex: integer, rowIndex: integer) {
const tileMap = this._renderer._tileMap;
if (!tileMap) {
if (!this._tileMap) {
return;
}
const layer = tileMap.getTileLayer(this._layerIndex);
const layer = this._tileMap.getTileLayer(this._layerIndex);
if (!layer) {
return;
}
@@ -789,24 +821,28 @@ namespace gdjs {
setGridRowCount(targetRowCount: integer) {
if (targetRowCount <= 0) return;
this._renderer.setGridRowCount(targetRowCount);
if (!this._tileMap) return;
this._tileMap.setDimensionY(targetRowCount);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
setGridColumnCount(targetColumnCount: integer) {
if (targetColumnCount <= 0) return;
this._renderer.setGridColumnCount(targetColumnCount);
if (!this._tileMap) return;
this._tileMap.setDimensionX(targetColumnCount);
this._isTileMapDirty = true;
this.invalidateHitboxes();
}
getGridRowCount(): integer {
return this._renderer.getGridRowCount();
if (!this._tileMap) return 0;
return this._tileMap.getDimensionY();
}
getGridColumnCount(): integer {
return this._renderer.getGridColumnCount();
if (!this._tileMap) return 0;
return this._tileMap.getDimensionX();
}
getTilesetColumnCount(): integer {
@@ -816,6 +852,73 @@ namespace gdjs {
getTilesetRowCount(): integer {
return this._rowCount;
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
getTileMapWidth() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getWidth() : 20;
}
getTileMapHeight() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getHeight() : 20;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
if (!this._tileMap) return -1;
return this._tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnY(x, y, layerIndex);
}
}
gdjs.registerObject(
'TileMap::SimpleTileMap',

View File

@@ -57,6 +57,19 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -189,9 +189,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TilemapCollisionMaskNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TilemapCollisionMaskNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
tmjf: this.getTilemapJsonFile(),
tsjf: this.getTilesetJsonFile(),
dm: this.getDebugMode(),
@@ -204,9 +206,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: TilemapCollisionMaskNetworkSyncData
networkSyncData: TilemapCollisionMaskNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.tmjf !== undefined) {
this.setTilemapJsonFile(networkSyncData.tmjf);

View File

@@ -10,8 +10,6 @@ namespace gdjs {
private _object:
| gdjs.TileMapRuntimeObject
| gdjs.SimpleTileMapRuntimeObject;
// TODO Move this attribute in the object as it's a model.
_tileMap: TileMapHelper.EditableTileMap | null = null;
private _pixiObject: PIXI.tilemap.CompositeTilemap;
@@ -51,40 +49,9 @@ namespace gdjs {
this._pixiObject.tileAnim[0] += 1;
}
updatePixiTileMap(
tileMap: TileMapHelper.EditableTileMap,
textureCache: TileMapHelper.TileTextureCache
) {
this._tileMap = tileMap;
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex
);
}
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
if (!this._tileMap) return;
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
this._tileMap,
textureCache,
// @ts-ignore
this._object._displayMode,
this._object._layerIndex
);
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
updatePosition(): void {
this._pixiObject.pivot.x = this.getTileMapWidth() / 2;
this._pixiObject.pivot.y = this.getTileMapHeight() / 2;
this._pixiObject.pivot.x = this._object.getTileMapWidth() / 2;
this._pixiObject.pivot.y = this._object.getTileMapHeight() / 2;
this._pixiObject.position.x = this._object.x + this.getWidth() / 2;
this._pixiObject.position.y = this._object.y + this.getHeight() / 2;
}
@@ -98,7 +65,7 @@ namespace gdjs {
// opacity. Setting alpha on each layer tile might not be useful as
// each layer would be separately transparent instead of the whole tilemap.
this._pixiObject.alpha = this._object._opacity / 255;
const tileMap = this._tileMap;
const tileMap = this._object.getTileMap();
if (!tileMap) return;
for (const layer of tileMap.getLayers()) {
if (
@@ -114,44 +81,34 @@ namespace gdjs {
}
}
getTileMapWidth() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getWidth() : 20;
}
getTileMapHeight() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getHeight() : 20;
}
setWidth(width: float): void {
this._pixiObject.scale.x = width / this.getTileMapWidth();
this._pixiObject.scale.x = width / this._object.getTileMapWidth();
this._pixiObject.position.x = this._object.x + width / 2;
}
setHeight(height: float): void {
this._pixiObject.scale.y = height / this.getTileMapHeight();
this._pixiObject.scale.y = height / this._object.getTileMapHeight();
this._pixiObject.position.y = this._object.y + height / 2;
}
setScaleX(scaleX: float): void {
this._pixiObject.scale.x = scaleX;
const width = scaleX * this.getTileMapWidth();
const width = scaleX * this._object.getTileMapWidth();
this._pixiObject.position.x = this._object.x + width / 2;
}
setScaleY(scaleY: float): void {
this._pixiObject.scale.y = scaleY;
const height = scaleY * this.getTileMapHeight();
const height = scaleY * this._object.getTileMapHeight();
this._pixiObject.position.y = this._object.y + height / 2;
}
getWidth(): float {
return this.getTileMapWidth() * this._pixiObject.scale.x;
return this._object.getTileMapWidth() * this._pixiObject.scale.x;
}
getHeight(): float {
return this.getTileMapHeight() * this._pixiObject.scale.y;
return this._object.getTileMapHeight() * this._pixiObject.scale.y;
}
getScaleX(): float {
@@ -162,91 +119,17 @@ namespace gdjs {
return this._pixiObject.scale.y;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
const tileMap = this._tileMap;
if (!tileMap) return -1;
return tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
const tileMap = this._tileMap;
refreshPixiTileMap(textureCache: TileMapHelper.TileTextureCache) {
const tileMap = this._object.getTileMap();
if (!tileMap) return;
tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
const tileMap = this._tileMap;
if (!tileMap) return;
tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
const tileMap = this._tileMap;
if (!tileMap) return false;
return tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
const tileMap = this._tileMap;
if (!tileMap) return false;
return tileMap.isTileFlippedOnY(x, y, layerIndex);
}
/**
* @param targetRowCount The number of rows to have.
*/
setGridRowCount(targetRowCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
return tileMap.setDimensionY(targetRowCount);
}
/**
* @param targetColumnCount The number of rows to have.
*/
setGridColumnCount(targetColumnCount: integer) {
const tileMap = this._tileMap;
if (!tileMap) return;
return tileMap.setDimensionX(targetColumnCount);
}
getGridRowCount(): integer {
const tileMap = this._tileMap;
if (!tileMap) return 0;
return tileMap.getDimensionY();
}
getGridColumnCount(): integer {
const tileMap = this._tileMap;
if (!tileMap) return 0;
return tileMap.getDimensionX();
TileMapHelper.PixiTileMapHelper.updatePixiTileMap(
this._pixiObject,
tileMap,
textureCache,
// @ts-ignore
this._displayMode,
this._object._layerIndex
);
}
destroy(): void {

View File

@@ -2,7 +2,6 @@
namespace gdjs {
export type TilemapObjectDataType = {
content: {
opacity: number;
tilemapJsonFile: string;
tilesetJsonFile: string;
tilemapAtlasImage: string;
@@ -38,7 +37,7 @@ namespace gdjs {
implements gdjs.Resizable, gdjs.Scalable, gdjs.OpacityHandler
{
_frameElapsedTime: float = 0;
_opacity: float;
_opacity: float = 255;
_tilemapJsonFile: string;
_tilesetJsonFile: string;
_tilemapAtlasImage: string;
@@ -48,11 +47,14 @@ namespace gdjs {
_animationSpeedScale: number;
_animationFps: number;
_tileMapManager: gdjs.TileMap.TileMapRuntimeManager;
_tileMap: TileMapHelper.EditableTileMap | null = null;
_renderer: gdjs.TileMapRuntimeObjectPixiRenderer;
constructor(instanceContainer: gdjs.RuntimeInstanceContainer, objectData) {
constructor(
instanceContainer: gdjs.RuntimeInstanceContainer,
objectData: TilemapObjectData
) {
super(instanceContainer, objectData);
this._opacity = objectData.content.opacity;
this._tilemapJsonFile = objectData.content.tilemapJsonFile;
this._tilesetJsonFile = objectData.content.tilesetJsonFile;
this._tilemapAtlasImage = objectData.content.tilemapAtlasImage;
@@ -93,9 +95,6 @@ namespace gdjs {
oldObjectData: TilemapObjectData,
newObjectData: TilemapObjectData
): boolean {
if (oldObjectData.content.opacity !== newObjectData.content.opacity) {
this.setOpacity(newObjectData.content.opacity);
}
if (
oldObjectData.content.tilemapJsonFile !==
newObjectData.content.tilemapJsonFile
@@ -145,9 +144,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TilemapNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TilemapNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
op: this._opacity,
tmjf: this._tilemapJsonFile,
tsjf: this._tilesetJsonFile,
@@ -159,8 +160,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(networkSyncData: TilemapNetworkSyncData): void {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(
networkSyncData: TilemapNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
@@ -232,7 +236,9 @@ namespace gdjs {
// getOrLoadTextureCache already log warns and errors.
return;
}
this._renderer.updatePixiTileMap(tileMap, textureCache);
this._tileMap = tileMap;
this._renderer.refreshPixiTileMap(textureCache);
this.invalidateHitboxes();
}
);
}
@@ -340,11 +346,11 @@ namespace gdjs {
}
override getOriginalWidth(): float {
return this._renderer.getTileMapWidth();
return this.getTileMapWidth();
}
override getOriginalHeight(): float {
return this._renderer.getTileMapHeight();
return this.getTileMapHeight();
}
/**
@@ -437,6 +443,73 @@ namespace gdjs {
getScaleY(): float {
return this._renderer.getScaleY();
}
getTileMap(): TileMapHelper.EditableTileMap | null {
return this._tileMap;
}
getTileMapWidth() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getWidth() : 20;
}
getTileMapHeight() {
const tileMap = this._tileMap;
return tileMap ? tileMap.getHeight() : 20;
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @returns The tile's id.
*/
getTileId(x: integer, y: integer, layerIndex: integer): integer {
if (!this._tileMap) return -1;
return this._tileMap.getTileId(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnY(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnY(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
* @param flip true if the tile should be flipped.
*/
flipTileOnX(x: integer, y: integer, layerIndex: integer, flip: boolean) {
if (!this._tileMap) return;
this._tileMap.flipTileOnX(x, y, layerIndex, flip);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnX(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnX(x, y, layerIndex);
}
/**
* @param x The layer column.
* @param y The layer row.
* @param layerIndex The layer index.
*/
isTileFlippedOnY(x: integer, y: integer, layerIndex: integer): boolean {
if (!this._tileMap) return false;
return this._tileMap.isTileFlippedOnY(x, y, layerIndex);
}
}
gdjs.registerObject('TileMap::TileMap', gdjs.TileMapRuntimeObject);
}

View File

@@ -82,9 +82,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): TiledSpriteNetworkSyncData {
getNetworkSyncData(
syncOptons: GetNetworkSyncDataOptions
): TiledSpriteNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptons),
xo: this.getXOffset(),
yo: this.getYOffset(),
op: this.getOpacity(),
@@ -93,9 +95,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: TiledSpriteNetworkSyncData
networkSyncData: TiledSpriteNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
// Texture is not synchronized, see if this is asked or not.

View File

@@ -34,6 +34,19 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () {
objects: [],
instances: [],
usedResources: [],
uiSettings: {
grid: false,
gridType: 'rectangular',
gridWidth: 10,
gridHeight: 10,
gridDepth: 10,
gridOffsetX: 0,
gridOffsetY: 0,
gridOffsetZ: 0,
gridColor: 0,
gridAlpha: 1,
snap: false,
},
},
usedExtensionsWithVariablesData: [],
});

View File

@@ -63,7 +63,7 @@ namespace gdjs {
// This is useful when the object is synchronized by an external source
// like in a multiplayer game, and we want to be able to predict the
// movement of the object, even if the inputs are not updated every frame.
_dontClearInputsBetweenFrames: boolean = false;
private _clearInputsBetweenFrames: boolean = true;
// This is useful when the object is synchronized over the network,
// object is controlled by the network and we want to ensure the current player
// cannot control it.
@@ -109,14 +109,16 @@ namespace gdjs {
: behaviorData.useLegacyTurnBack;
}
getNetworkSyncData(): TopDownMovementNetworkSyncData {
getNetworkSyncData(
options: GetNetworkSyncDataOptions
): TopDownMovementNetworkSyncData {
// This method is called, so we are synchronizing this object.
// Let's clear the inputs between frames as we control it.
this._dontClearInputsBetweenFrames = false;
this._clearInputsBetweenFrames = true;
this._ignoreDefaultControlsAsSyncedByNetwork = false;
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(options),
props: {
a: this._angle,
xv: this._xVelocity,
@@ -134,9 +136,10 @@ namespace gdjs {
}
updateFromNetworkSyncData(
networkSyncData: TopDownMovementNetworkSyncData
networkSyncData: TopDownMovementNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
const behaviorSpecificProps = networkSyncData.props;
if (behaviorSpecificProps.a !== undefined) {
@@ -173,10 +176,10 @@ namespace gdjs {
this._stickForce = behaviorSpecificProps.sf;
}
// When the object is synchronized from the network, the inputs must not be cleared.
this._dontClearInputsBetweenFrames = true;
// And we are not using the default controls.
this._ignoreDefaultControlsAsSyncedByNetwork = true;
// Clear user inputs between frames only if requested.
this._clearInputsBetweenFrames = !!options.clearInputs;
// And ignore default controls if not asked otherwise.
this._ignoreDefaultControlsAsSyncedByNetwork = !options.keepControl;
}
updateFromBehaviorData(oldBehaviorData, newBehaviorData): boolean {
@@ -581,7 +584,7 @@ namespace gdjs {
this._wasRightKeyPressed = this._rightKey;
this._wasUpKeyPressed = this._upKey;
this._wasDownKeyPressed = this._downKey;
if (!this._dontClearInputsBetweenFrames) {
if (this._clearInputsBetweenFrames) {
this._leftKey = false;
this._rightKey = false;
this._upKey = false;

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,15 @@ Copyright (c) 2010-2023 Florian Rival (Florian.Rival@gmail.com)
*/
namespace gdjs {
const logger = new gdjs.Logger('Tween');
interface TweenBehaviorNetworkSyncDataType {
tweenManager: TweenManagerNetworkSyncData;
}
export interface TweenBehaviorNetworkSyncData
extends BehaviorNetworkSyncData {
props: TweenBehaviorNetworkSyncDataType;
}
interface IColorable extends gdjs.RuntimeObject {
setColor(color: string): void;
getColor(): string;
@@ -59,6 +68,305 @@ namespace gdjs {
const exponentialInterpolation =
gdjs.evtTools.common.exponentialInterpolation;
const tweenObjectValueSetter = () => {};
const getTweenVariableSetter = (variable: gdjs.Variable) => {
return (value: float) => variable.setNumber(value);
};
const getTweenObjectPositionXSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setX(value);
};
const getTweenObjectPositionYSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setY(value);
};
const getTweenObjectPositionZSetter = (object: gdjs.RuntimeObject) => {
if (!is3D(object)) return () => {};
return (value: float) => object.setZ(value);
};
const getTweenObjectPositionSetter = (object: gdjs.RuntimeObject) => {
return ([x, y]: float[]) => object.setPosition(x, y);
};
const getTweenObjectAngleSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setAngle(value);
};
const getTweenObjectWidthSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setWidth(value);
};
const getTweenObjectHeightSetter = (object: gdjs.RuntimeObject) => {
return (value: float) => object.setHeight(value);
};
const getTweenObjectRotationXSetter = (
object: gdjs.RuntimeObject & gdjs.Base3DHandler
) => {
return (value: float) => object.setRotationX(value);
};
const getTweenObjectRotationYSetter = (
object: gdjs.RuntimeObject & gdjs.Base3DHandler
) => {
return (value: float) => object.setRotationY(value);
};
const getTweenObjectDepthSetter = (
object: gdjs.RuntimeObject & gdjs.Base3DHandler
) => {
return (value: float) => object.setDepth(value);
};
const getTweenObjectScaleXYSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? ([scaleX, scaleY]: float[]) => {
const oldX = object.getCenterXInScene();
const oldY = object.getCenterYInScene();
object.setScaleX(scaleX);
object.setScaleY(scaleY);
object.setCenterPositionInScene(oldX, oldY);
}
: ([scaleX, scaleY]: float[]) => {
object.setScaleX(scaleX);
object.setScaleY(scaleY);
};
};
const getTweenObjectScaleSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
object3d: (gdjs.RuntimeObject & gdjs.Base3DHandler) | null,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? (scale: float) => {
const oldX = object.getCenterXInScene();
const oldY = object.getCenterYInScene();
const oldZ = object3d ? object3d.getCenterZInScene() : 0;
object.setScale(scale);
object.setCenterXInScene(oldX);
object.setCenterYInScene(oldY);
if (object3d) {
object3d.setCenterZInScene(oldZ);
}
}
: (scale: float) => object.setScale(scale);
};
const getTweenObjectScaleXSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? (scaleX: float) => {
const oldX = object.getCenterXInScene();
object.setScaleX(scaleX);
object.setCenterXInScene(oldX);
}
: (scaleX: float) => object.setScaleX(scaleX);
};
const getTweenObjectScaleYSetter = (
object: gdjs.RuntimeObject & gdjs.Scalable,
scaleFromCenterOfObject: boolean
) => {
return scaleFromCenterOfObject
? (scaleY: float) => {
const oldY = object.getCenterYInScene();
object.setScaleY(scaleY);
object.setCenterYInScene(oldY);
}
: (scaleY: float) => object.setScaleY(scaleY);
};
const getTweenObjectOpacitySetter = (
object: gdjs.RuntimeObject & gdjs.OpacityHandler
) => {
return (value: float) => object.setOpacity(value);
};
const getTweenObjectCharacterSizeSetter = (object: ICharacterScalable) => {
return (value: float) => object.setCharacterSize(value);
};
const getTweenObjectNumberEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return (value: float) => {
effect.updateDoubleParameter(propertyName, value);
};
};
const getTweenObjectColorEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return ([hue, saturation, lightness]: number[]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
};
};
const getTweenObjectColorSetter = (
object: IColorable,
useHSLColorTransition: boolean
) => {
if (useHSLColorTransition) {
return ([hue, saturation, lightness]: number[]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
object.setColor(
Math.floor(rgbFromHslColor[0]) +
';' +
Math.floor(rgbFromHslColor[1]) +
';' +
Math.floor(rgbFromHslColor[2])
);
};
} else {
return ([red, green, blue]: number[]) => {
object.setColor(
Math.floor(red) + ';' + Math.floor(green) + ';' + Math.floor(blue)
);
};
}
};
const getTweenObjectColorHSLSetter = (object: IColorable) => {
return ([hue, saturation, lightness]: number[]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
object.setColor(
Math.floor(rgbFromHslColor[0]) +
';' +
Math.floor(rgbFromHslColor[1]) +
';' +
Math.floor(rgbFromHslColor[2])
);
};
};
const tweenSetterFactory =
(object: RuntimeObject) =>
(tweenInformation: TweenInformationNetworkSyncData) => {
const type = tweenInformation.type;
const variablePath = tweenInformation.variablePath;
const effectName = tweenInformation.effectName;
const propertyName = tweenInformation.propertyName;
const scaleFromCenterOfObject =
!!tweenInformation.scaleFromCenterOfObject;
const useHSLColorTransition = !!tweenInformation.useHSLColorTransition;
if (type === 'objectValue') {
return tweenObjectValueSetter;
}
if (type === 'variable' && variablePath) {
const variable = object
.getVariables()
.getVariableFromPath(variablePath);
if (!variable) return () => {};
return getTweenVariableSetter(variable);
}
if (type === 'positionX') {
return getTweenObjectPositionXSetter(object);
}
if (type === 'positionY') {
return getTweenObjectPositionYSetter(object);
}
if (type === 'position') {
return getTweenObjectPositionSetter(object);
}
if (type === 'positionZ') {
return getTweenObjectPositionZSetter(object);
}
if (type === 'width') {
return getTweenObjectWidthSetter(object);
}
if (type === 'height') {
return getTweenObjectHeightSetter(object);
}
if (type === 'depth') {
if (!is3D(object)) return () => {};
return getTweenObjectDepthSetter(object);
}
if (type === 'angle') {
return getTweenObjectAngleSetter(object);
}
if (type === 'rotationX') {
if (!is3D(object)) return () => {};
return getTweenObjectRotationXSetter(object);
}
if (type === 'rotationY') {
if (!is3D(object)) return () => {};
return getTweenObjectRotationYSetter(object);
}
if (type === 'scale') {
if (!isScalable(object)) return () => {};
const object3d = is3D(object) ? object : null;
return getTweenObjectScaleSetter(
object,
object3d,
scaleFromCenterOfObject
);
}
if (type === 'scaleXY') {
if (!isScalable(object)) return () => {};
return getTweenObjectScaleXYSetter(object, scaleFromCenterOfObject);
}
if (type === 'scaleX') {
if (!isScalable(object)) return () => {};
return getTweenObjectScaleXSetter(object, scaleFromCenterOfObject);
}
if (type === 'scaleY') {
if (!isScalable(object)) return () => {};
return getTweenObjectScaleYSetter(object, scaleFromCenterOfObject);
}
if (type === 'opacity') {
if (!isOpaque(object)) return () => {};
return getTweenObjectOpacitySetter(object);
}
if (type === 'characterSize') {
if (!isCharacterScalable(object)) return () => {};
return getTweenObjectCharacterSizeSetter(object);
}
if (type === 'numberEffectProperty' && effectName && propertyName) {
const effect = object.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The object "${object.name}" doesn't have any effect called "${effectName}"`
);
}
return getTweenObjectNumberEffectPropertySetter(effect, propertyName);
}
if (type === 'colorEffectProperty' && effectName && propertyName) {
const effect = object.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The object "${object.name}" doesn't have any effect called "${effectName}"`
);
}
return getTweenObjectColorEffectPropertySetter(effect, propertyName);
}
if (type === 'objectColor') {
if (!isColorable(object)) return () => {};
return getTweenObjectColorSetter(object, useHSLColorTransition);
}
if (type === 'objectColorHSL') {
if (!isColorable(object)) return () => {};
return getTweenObjectColorHSLSetter(object);
}
return () => {};
};
export class TweenRuntimeBehavior extends gdjs.RuntimeBehavior {
private _tweens = new gdjs.evtTools.tween.TweenManager();
private _isActive: boolean = true;
@@ -84,6 +392,43 @@ namespace gdjs {
return true;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): TweenBehaviorNetworkSyncData {
return {
...super.getNetworkSyncData(syncOptions),
props: {
tweenManager: this._tweens.getNetworkSyncData(),
},
};
}
updateFromNetworkSyncData(
networkSyncData: TweenBehaviorNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.props.tweenManager) {
this._tweens.updateFromNetworkSyncData(
networkSyncData.props.tweenManager,
(tweenInformationNetworkSyncData) => {
return this.owner;
},
(tweenInformationNetworkSyncData) => {
return tweenSetterFactory(this.owner)(
tweenInformationNetworkSyncData
);
},
(tweenInformationNetworkSyncData) => {
return tweenInformationNetworkSyncData.destroyObjectWhenFinished
? () => this._deleteFromScene()
: null;
}
);
}
}
doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer): void {
this._tweens.step();
}
@@ -126,7 +471,12 @@ namespace gdjs {
linearInterpolation,
fromValue,
toValue,
(value: float) => variable.setNumber(value),
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -208,7 +558,12 @@ namespace gdjs {
linearInterpolation,
variable.getValue() as number,
toValue,
(value: float) => variable.setNumber(value),
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -243,7 +598,11 @@ namespace gdjs {
: linearInterpolation,
fromValue,
toValue,
(value: float) => {},
tweenObjectValueSetter,
{
type: 'objectValue',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -322,7 +681,11 @@ namespace gdjs {
linearInterpolation,
[this.owner.getX(), this.owner.getY()],
[toX, toY],
([x, y]) => this.owner.setPosition(x, y),
getTweenObjectPositionSetter(this.owner),
{
type: 'position',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -394,7 +757,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getX(),
toX,
(value: float) => this.owner.setX(value),
getTweenObjectPositionXSetter(this.owner),
{
type: 'positionX',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -466,7 +833,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getY(),
toY,
(value: float) => this.owner.setY(value),
getTweenObjectPositionYSetter(this.owner),
{
type: 'positionY',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -543,7 +914,11 @@ namespace gdjs {
linearInterpolation,
owner.getZ(),
toZ,
(value: float) => owner.setZ(value),
getTweenObjectPositionZSetter(owner),
{
type: 'positionZ',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -615,7 +990,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getAngle(),
toAngle,
(value: float) => this.owner.setAngle(value),
getTweenObjectAngleSetter(this.owner),
{
type: 'angle',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -648,7 +1027,11 @@ namespace gdjs {
linearInterpolation,
owner.getRotationX(),
toAngle,
(value: float) => owner.setRotationX(value),
getTweenObjectRotationXSetter(owner),
{
type: 'rotationX',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -681,7 +1064,11 @@ namespace gdjs {
linearInterpolation,
owner.getRotationY(),
toAngle,
(value: float) => owner.setRotationY(value),
getTweenObjectRotationYSetter(owner),
{
type: 'rotationY',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -769,19 +1156,6 @@ namespace gdjs {
if (toScaleX < 0) toScaleX = 0;
if (toScaleY < 0) toScaleY = 0;
const setValue = scaleFromCenterOfObject
? ([scaleX, scaleY]: float[]) => {
const oldX = owner.getCenterXInScene();
const oldY = owner.getCenterYInScene();
owner.setScaleX(scaleX);
owner.setScaleY(scaleY);
owner.setCenterPositionInScene(oldX, oldY);
}
: ([scaleX, scaleY]: float[]) => {
owner.setScaleX(scaleX);
owner.setScaleY(scaleY);
};
this._tweens.addMultiTween(
identifier,
timeSource,
@@ -790,7 +1164,11 @@ namespace gdjs {
interpolation,
[owner.getScaleX(), owner.getScaleY()],
[toScaleX, toScaleY],
setValue,
getTweenObjectScaleXYSetter(owner, scaleFromCenterOfObject),
{
type: 'scaleXY',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -830,20 +1208,6 @@ namespace gdjs {
// when the 3D extension is not used.
const owner3d = is3D(owner) ? owner : null;
const setValue = scaleFromCenterOfObject
? (scale: float) => {
const oldX = owner.getCenterXInScene();
const oldY = owner.getCenterYInScene();
const oldZ = owner3d ? owner3d.getCenterZInScene() : 0;
owner.setScale(scale);
owner.setCenterXInScene(oldX);
owner.setCenterYInScene(oldY);
if (owner3d) {
owner3d.setCenterZInScene(oldZ);
}
}
: (scale: float) => owner.setScale(scale);
this._tweens.addSimpleTween(
identifier,
this.owner,
@@ -852,7 +1216,12 @@ namespace gdjs {
exponentialInterpolation,
owner.getScale(),
toScale,
setValue,
getTweenObjectScaleSetter(owner, owner3d, scaleFromCenterOfObject),
{
type: 'scale',
scaleFromCenterOfObject,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -929,14 +1298,6 @@ namespace gdjs {
const owner = this.owner;
if (!isScalable(owner)) return;
const setValue = scaleFromCenterOfObject
? (scaleX: float) => {
const oldX = owner.getCenterXInScene();
owner.setScaleX(scaleX);
owner.setCenterXInScene(oldX);
}
: (scaleX: float) => owner.setScaleX(scaleX);
this._tweens.addSimpleTween(
identifier,
timeSource,
@@ -945,7 +1306,12 @@ namespace gdjs {
interpolation,
owner.getScaleX(),
toScaleX,
setValue,
getTweenObjectScaleXSetter(owner, scaleFromCenterOfObject),
{
type: 'scaleX',
scaleFromCenterOfObject,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1022,14 +1388,6 @@ namespace gdjs {
const owner = this.owner;
if (!isScalable(owner)) return;
const setValue = scaleFromCenterOfObject
? (scaleY: float) => {
const oldY = owner.getCenterYInScene();
owner.setScaleY(scaleY);
owner.setCenterYInScene(oldY);
}
: (scaleY: float) => owner.setScaleY(scaleY);
this._tweens.addSimpleTween(
identifier,
timeSource,
@@ -1038,7 +1396,12 @@ namespace gdjs {
interpolation,
owner.getScaleY(),
toScaleY,
setValue,
getTweenObjectScaleYSetter(owner, scaleFromCenterOfObject),
{
type: 'scaleY',
scaleFromCenterOfObject,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1113,7 +1476,11 @@ namespace gdjs {
linearInterpolation,
owner.getOpacity(),
toOpacity,
(value: float) => owner.setOpacity(value),
getTweenObjectOpacitySetter(owner),
{
type: 'opacity',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1153,10 +1520,12 @@ namespace gdjs {
linearInterpolation,
effect ? effect.getDoubleParameter(propertyName) : 0,
toValue,
(value: float) => {
if (effect) {
effect.updateDoubleParameter(propertyName, value);
}
getTweenObjectNumberEffectPropertySetter(effect, propertyName),
{
type: 'numberEffectProperty',
effectName,
propertyName,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
@@ -1210,22 +1579,12 @@ namespace gdjs {
rgbToColor[1],
rgbToColor[2]
),
([hue, saturation, lightness]) => {
if (effect) {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
}
getTweenObjectColorEffectPropertySetter(effect, propertyName),
{
type: 'colorEffectProperty',
effectName,
propertyName,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
@@ -1305,43 +1664,20 @@ namespace gdjs {
const rgbFromColor: float[] = gdjs.rgbOrHexToRGBColor(owner.getColor());
const rgbToColor: float[] = gdjs.rgbOrHexToRGBColor(toColorStr);
let initialValue;
let targetedValue;
let setValue;
if (useHSLColorTransition) {
initialValue = gdjs.evtTools.tween.rgbToHsl(
rgbFromColor[0],
rgbFromColor[1],
rgbFromColor[2]
);
targetedValue = gdjs.evtTools.tween.rgbToHsl(
rgbToColor[0],
rgbToColor[1],
rgbToColor[2]
);
setValue = ([hue, saturation, lightness]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
owner.setColor(
Math.round(rgbFromHslColor[0]) +
';' +
Math.round(rgbFromHslColor[1]) +
';' +
Math.round(rgbFromHslColor[2])
);
};
} else {
initialValue = rgbFromColor;
targetedValue = rgbToColor;
setValue = ([red, green, blue]) => {
owner.setColor(
Math.floor(red) + ';' + Math.floor(green) + ';' + Math.floor(blue)
);
};
}
const initialValue = useHSLColorTransition
? gdjs.evtTools.tween.rgbToHsl(
rgbFromColor[0],
rgbFromColor[1],
rgbFromColor[2]
)
: rgbFromColor;
const targetedValue = useHSLColorTransition
? gdjs.evtTools.tween.rgbToHsl(
rgbToColor[0],
rgbToColor[1],
rgbToColor[2]
)
: rgbToColor;
this._tweens.addMultiTween(
identifier,
@@ -1351,7 +1687,12 @@ namespace gdjs {
linearInterpolation,
initialValue,
targetedValue,
setValue,
getTweenObjectColorSetter(owner, useHSLColorTransition),
{
type: 'objectColor',
useHSLColorTransition,
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1462,25 +1803,13 @@ namespace gdjs {
duration,
easing,
linearInterpolation,
hslFromColor,
[toH, toS, toL],
([hue, saturation, lightness]) => {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
owner.setColor(
Math.round(rgbFromHslColor[0]) +
';' +
Math.round(rgbFromHslColor[1]) +
';' +
Math.round(rgbFromHslColor[2])
);
getTweenObjectColorHSLSetter(owner),
{
type: 'objectColorHSL',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1558,7 +1887,11 @@ namespace gdjs {
interpolation,
owner.getCharacterSize(),
toSize,
(value: float) => owner.setCharacterSize(value),
getTweenObjectCharacterSizeSetter(owner),
{
type: 'characterSize',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1630,7 +1963,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getWidth(),
toWidth,
(value: float) => this.owner.setWidth(value),
getTweenObjectWidthSetter(this.owner),
{
type: 'width',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1702,7 +2039,11 @@ namespace gdjs {
linearInterpolation,
this.owner.getHeight(),
toHeight,
(value: float) => this.owner.setHeight(value),
getTweenObjectHeightSetter(this.owner),
{
type: 'height',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}
@@ -1779,7 +2120,11 @@ namespace gdjs {
linearInterpolation,
owner.getDepth(),
toDepth,
(value: float) => owner.setDepth(value),
getTweenObjectDepthSetter(owner),
{
type: 'depth',
destroyObjectWhenFinished,
},
destroyObjectWhenFinished ? () => this._deleteFromScene() : null
);
}

View File

@@ -10,6 +10,127 @@ namespace gdjs {
export namespace tween {
const logger = new gdjs.Logger('Tween');
const getTweenVariableSetter = (variable: gdjs.Variable) => {
return (value: float) => variable.setNumber(value);
};
const tweenLayoutValueSetter = (value: float) => {};
const tweenLayerValueSetter = (value: float) => {};
const getTweenLayerCameraPositionSetter = (layer: gdjs.RuntimeLayer) => {
return ([x, y]: Array<float>) => {
layer.setCameraX(x);
layer.setCameraY(y);
};
};
const getTweenLayerCameraRotationSetter = (layer: gdjs.RuntimeLayer) => {
return (value: float) => layer.setCameraRotation(value);
};
const getTweenLayerCameraZoomSetter = (layer: gdjs.RuntimeLayer) => {
return (value: float) => layer.setCameraZoom(value);
};
const getTweenNumberEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return (value: float) => {
if (effect) {
effect.updateDoubleParameter(propertyName, value);
}
};
};
const getTweenColorEffectPropertySetter = (
effect: PixiFiltersTools.Filter,
propertyName: string
) => {
return ([hue, saturation, lightness]: Array<float>) => {
if (effect) {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
}
};
};
// Factory to get the tween setter based on type and options
export const tweenSetterFactory =
(runtimeScene: RuntimeScene) =>
(tweenInformation: TweenInformationNetworkSyncData) => {
const type = tweenInformation.type;
const layerName = tweenInformation.layerName;
const variablePath = tweenInformation.variablePath;
const effectName = tweenInformation.effectName;
const propertyName = tweenInformation.propertyName;
if (type === 'variable' && variablePath) {
const variable = runtimeScene
.getVariables()
.getVariableFromPath(variablePath);
if (!variable) {
return () => {};
}
return getTweenVariableSetter(variable);
}
if (type === 'cameraZoom' && layerName !== undefined) {
const layer = runtimeScene.getLayer(layerName);
return getTweenLayerCameraZoomSetter(layer);
}
if (type === 'cameraRotation' && layerName !== undefined) {
const layer = runtimeScene.getLayer(layerName);
return getTweenLayerCameraRotationSetter(layer);
}
if (type === 'cameraPosition' && layerName !== undefined) {
const layer = runtimeScene.getLayer(layerName);
return getTweenLayerCameraPositionSetter(layer);
}
if (
type === 'colorEffectProperty' &&
layerName !== undefined &&
effectName &&
propertyName
) {
const layer = runtimeScene.getLayer(layerName);
const effect = layer.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The layer "${layerName}" doesn't have any effect called "${effectName}"`
);
}
return getTweenColorEffectPropertySetter(effect, propertyName);
}
if (
type === 'numberEffectProperty' &&
layerName !== undefined &&
effectName &&
propertyName
) {
const layer = runtimeScene.getLayer(layerName);
const effect = layer.getRendererEffects()[effectName];
if (!effect) {
logger.error(
`The layer "${layerName}" doesn't have any effect called "${effectName}"`
);
}
return getTweenNumberEffectPropertySetter(effect, propertyName);
}
if (type === 'layoutValue') {
return tweenLayoutValueSetter;
}
if (type === 'layerValue') {
return tweenLayerValueSetter;
}
return () => {};
};
export const getTweensMap = (runtimeScene: RuntimeScene) =>
runtimeScene._tweens ||
(runtimeScene._tweens = new gdjs.evtTools.tween.TweenManager());
@@ -20,6 +141,44 @@ namespace gdjs {
gdjs.evtTools.tween.getTweensMap(runtimeScene).step();
});
gdjs.registerRuntimeSceneGetSyncDataCallback(
function (runtimeScene, currentLayoutSyncData, syncOptions) {
if (!syncOptions.syncTweens) return;
const tweensNetworkSyncData = gdjs.evtTools.tween
.getTweensMap(runtimeScene)
.getNetworkSyncData();
currentLayoutSyncData.tween = tweensNetworkSyncData;
}
);
gdjs.registerRuntimeSceneUpdateFromSyncDataCallback(
function (runtimeScene, receivedSyncData, syncOptions) {
if (!receivedSyncData.tween) return;
gdjs.evtTools.tween
.getTweensMap(runtimeScene)
.updateFromNetworkSyncData(
receivedSyncData.tween,
(tweenInformationNetworkSyncData) => {
if (tweenInformationNetworkSyncData.layerName !== undefined) {
return runtimeScene.getLayer(
tweenInformationNetworkSyncData.layerName
);
}
return runtimeScene;
},
(tweenInformationNetworkSyncData) => {
return gdjs.evtTools.tween.tweenSetterFactory(runtimeScene)(
tweenInformationNetworkSyncData
);
},
// No onFinish for scene tweens.
() => null
);
}
);
export const sceneTweenExists = (
runtimeScene: RuntimeScene,
id: string
@@ -130,7 +289,10 @@ namespace gdjs {
: linearInterpolation,
fromValue,
toValue,
(value: float) => {}
tweenLayoutValueSetter,
{
type: 'layoutValue',
}
);
};
@@ -167,7 +329,11 @@ namespace gdjs {
: linearInterpolation,
fromValue,
toValue,
(value: float) => {}
tweenLayerValueSetter,
{
type: 'layerValue',
layerName,
}
);
};
@@ -197,7 +363,11 @@ namespace gdjs {
linearInterpolation,
from,
to,
(value: float) => variable.setNumber(value)
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
}
);
};
@@ -250,7 +420,11 @@ namespace gdjs {
linearInterpolation,
variable.getValue() as number,
toValue,
(value: float) => variable.setNumber(value)
getTweenVariableSetter(variable),
{
type: 'variable',
variable,
}
);
};
@@ -329,9 +503,10 @@ namespace gdjs {
linearInterpolation,
[layer.getCameraX(), layer.getCameraY()],
[toX, toY],
([x, y]) => {
layer.setCameraX(x);
layer.setCameraY(y);
getTweenLayerCameraPositionSetter(layer),
{
type: 'cameraPosition',
layerName,
}
);
};
@@ -408,7 +583,11 @@ namespace gdjs {
interpolation,
layer.getCameraZoom(),
toZoom,
(value: float) => layer.setCameraZoom(value)
getTweenLayerCameraZoomSetter(layer),
{
type: 'cameraZoom',
layerName,
}
);
};
@@ -478,7 +657,11 @@ namespace gdjs {
linearInterpolation,
layer.getCameraRotation(),
toRotation,
(value: float) => layer.setCameraRotation(value)
getTweenLayerCameraRotationSetter(layer),
{
type: 'cameraRotation',
layerName,
}
);
};
@@ -518,10 +701,12 @@ namespace gdjs {
linearInterpolation,
effect ? effect.getDoubleParameter(propertyName) : 0,
toValue,
(value: float) => {
if (effect) {
effect.updateDoubleParameter(propertyName, value);
}
getTweenNumberEffectPropertySetter(effect, propertyName),
{
type: 'numberEffectProperty',
layerName,
effectName,
propertyName,
}
);
};
@@ -575,22 +760,12 @@ namespace gdjs {
rgbToColor[1],
rgbToColor[2]
),
([hue, saturation, lightness]) => {
if (effect) {
const rgbFromHslColor = gdjs.evtTools.tween.hslToRgb(
hue,
saturation,
lightness
);
effect.updateColorParameter(
propertyName,
gdjs.rgbToHexNumber(
rgbFromHslColor[0],
rgbFromHslColor[1],
rgbFromHslColor[2]
)
);
}
getTweenColorEffectPropertySetter(effect, propertyName),
{
type: 'colorEffectProperty',
layerName,
effectName,
propertyName,
}
);
};

View File

@@ -16,7 +16,7 @@ namespace gdjs {
export type VideoObjectData = ObjectData & VideoObjectDataType;
export type VideoNetworkSyncDataType = {
export type VideoObjectNetworkSyncDataType = {
op: float;
// We don't sync volume, as it's probably a user setting?
pla: boolean;
@@ -25,8 +25,8 @@ namespace gdjs {
ps: number;
};
export type VideoNetworkSyncData = ObjectNetworkSyncData &
VideoNetworkSyncDataType;
export type VideoObjectNetworkSyncData = ObjectNetworkSyncData &
VideoObjectNetworkSyncDataType;
/**
* An object displaying a video on screen.
@@ -99,9 +99,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): VideoNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): VideoObjectNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
op: this._opacity,
pla: this.isPlayed(),
loop: this.isLooped(),
@@ -110,8 +112,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(syncData: VideoNetworkSyncData): void {
super.updateFromNetworkSyncData(syncData);
updateFromNetworkSyncData(
syncData: VideoObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
super.updateFromNetworkSyncData(syncData, options);
if (this._opacity !== undefined && this._opacity && syncData.op) {
this.setOpacity(syncData.op);

View File

@@ -214,16 +214,16 @@ CODE_NAMESPACE.RUNTIME_BEHAVIOR_CLASSNAME = class RUNTIME_BEHAVIOR_CLASSNAME ext
}
// Network sync:
getNetworkSyncData() {
getNetworkSyncData(syncOptions) {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
props: {
GET_NETWORK_SYNC_DATA_CODE
}
};
}
updateFromNetworkSyncData(networkSyncData) {
super.updateFromNetworkSyncData(networkSyncData);
updateFromNetworkSyncData(networkSyncData, options) {
super.updateFromNetworkSyncData(networkSyncData, options);
UPDATE_FROM_NETWORK_SYNC_DATA_CODE
}

View File

@@ -74,10 +74,15 @@ gd::String EventsCodeGenerator::GenerateEventsListCompleteFunctionCode(
codeGenerator.GetCodeNamespace() + ".localVariables = [];\n";
}
gd::String idToCallbackMapCode;
idToCallbackMapCode +=
codeGenerator.GetCodeNamespace() + ".idToCallbackMap = new Map();\n";
gd::String output =
// clang-format off
codeGenerator.GetCodeNamespace() + " = {};\n" +
localVariablesInitializationCode +
idToCallbackMapCode +
globalDeclarations +
globalObjectLists + "\n\n" +
codeGenerator.GetCustomCodeOutsideMain() + "\n\n" +
@@ -135,8 +140,11 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionCode(
gd::VariablesContainer::SourceType::Parameters);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForFreeEventsFunction(
project, eventsFunctionsExtension, eventsFunction,
parameterObjectsAndGroups, parameterVariablesContainer);
project,
eventsFunctionsExtension,
eventsFunction,
parameterObjectsAndGroups,
parameterVariablesContainer);
EventsCodeGenerator codeGenerator(projectScopedContainers);
codeGenerator.SetCodeNamespace(codeNamespace);
@@ -148,16 +156,21 @@ gd::String EventsCodeGenerator::GenerateEventsFunctionCode(
// Generate the code setting up the context of the function.
gd::String fullPreludeCode = "let scopeInstanceContainer = null;\n" +
codeGenerator.GenerateFreeEventsFunctionContext(
eventsFunctionsExtension, eventsFunction,
eventsFunctionsExtension,
eventsFunction,
"runtimeScene.getOnceTriggers()");
gd::String output = GenerateEventsListCompleteFunctionCode(
codeGenerator, codeGenerator.GetCodeNamespaceAccessor() + "func",
codeGenerator,
codeGenerator.GetCodeNamespaceAccessor() + "func",
codeGenerator.GenerateEventsFunctionParameterDeclarationsList(
eventsFunction.GetParametersForEvents(
eventsFunctionsExtension.GetEventsFunctions()),
0, true),
fullPreludeCode, eventsFunction.GetEvents(), "",
0,
true),
fullPreludeCode,
eventsFunction.GetEvents(),
"",
codeGenerator.GenerateEventsFunctionReturn(eventsFunction));
// TODO: the editor should pass the diagnostic report and display it to the
@@ -193,9 +206,13 @@ gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionCode(
gd::VariablesContainer::SourceType::Properties);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForBehaviorEventsFunction(
project, eventsFunctionsExtension, eventsBasedBehavior,
eventsFunction, parameterObjectsContainers,
parameterVariablesContainer, propertyVariablesContainer);
project,
eventsFunctionsExtension,
eventsBasedBehavior,
eventsFunction,
parameterObjectsContainers,
parameterVariablesContainer,
propertyVariablesContainer);
EventsCodeGenerator codeGenerator(projectScopedContainers);
codeGenerator.SetCodeNamespace(codeNamespace);
@@ -209,8 +226,9 @@ gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionCode(
preludeCode + "\n" + "var that = this;\n" +
// runtimeScene is supposed to be always accessible, read
// it from the behavior.
// TODO: this should be renamed to "instanceContainer" and have the code generation
// adapted for this (rely less on `gdjs.RuntimeScene`, and more on `RuntimeInstanceContainer`).
// TODO: this should be renamed to "instanceContainer" and have the code
// generation adapted for this (rely less on `gdjs.RuntimeScene`, and more
// on `RuntimeInstanceContainer`).
"var runtimeScene = this._runtimeScene;\n" +
"let scopeInstanceContainer = null;\n" +
// By convention of Behavior Events Function, the object is accessible
@@ -279,8 +297,12 @@ gd::String EventsCodeGenerator::GenerateObjectEventsFunctionCode(
gd::VariablesContainer::SourceType::Properties);
auto projectScopedContainers = gd::ProjectScopedContainers::
MakeNewProjectScopedContainersForObjectEventsFunction(
project, eventsFunctionsExtension, eventsBasedObject, eventsFunction,
parameterObjectsContainers, parameterVariablesContainer,
project,
eventsFunctionsExtension,
eventsBasedObject,
eventsFunction,
parameterObjectsContainers,
parameterVariablesContainer,
propertyVariablesContainer);
EventsCodeGenerator codeGenerator(projectScopedContainers);
@@ -295,8 +317,9 @@ gd::String EventsCodeGenerator::GenerateObjectEventsFunctionCode(
preludeCode + "\n" + "var that = this;\n" +
// runtimeScene is supposed to be always accessible, read
// it from the object.
// TODO: this should be renamed to "instanceContainer" and have the code generation
// adapted for this (rely less on `gdjs.RuntimeScene`, and more on `RuntimeInstanceContainer`).
// TODO: this should be renamed to "instanceContainer" and have the code
// generation adapted for this (rely less on `gdjs.RuntimeScene`, and more
// on `RuntimeInstanceContainer`).
"var runtimeScene = this._instanceContainer;\n" +
"let scopeInstanceContainer = this._instanceContainer;\n" +
// By convention of Object Events Function, the object is accessible
@@ -388,13 +411,14 @@ gd::String EventsCodeGenerator::GenerateFreeEventsFunctionContext(
gd::String objectsGettersMap;
gd::String objectArraysMap;
gd::String behaviorNamesMap;
return GenerateEventsFunctionContext(eventsFunctionsExtension,
eventsFunctionsExtension.GetEventsFunctions(),
eventsFunction,
onceTriggersVariable,
objectsGettersMap,
objectArraysMap,
behaviorNamesMap);
return GenerateEventsFunctionContext(
eventsFunctionsExtension,
eventsFunctionsExtension.GetEventsFunctions(),
eventsFunction,
onceTriggersVariable,
objectsGettersMap,
objectArraysMap,
behaviorNamesMap);
}
gd::String EventsCodeGenerator::GenerateBehaviorEventsFunctionContext(
@@ -929,7 +953,8 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
gd::String actionCode;
// Prepare call
@@ -978,10 +1003,11 @@ gd::String EventsCodeGenerator::GenerateObjectAction(
actionCode += " " + call + ";\n";
actionCode += "}\n";
if (!optionalAsyncCallbackName.empty()) {
if (!optionalAsyncCallbackName.empty() && !optionalAsyncCallbackId.empty()) {
actionCode +=
"runtimeScene.getAsyncTasksManager().addTask(asyncTaskGroup, " +
optionalAsyncCallbackName + ")\n}";
optionalAsyncCallbackName + ", " + optionalAsyncCallbackId +
", asyncObjectsList)\n}";
}
return actionCode;
@@ -995,7 +1021,8 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName) {
const gd::String& optionalAsyncCallbackName,
const gd::String& optionalAsyncCallbackId) {
gd::String actionCode;
// Prepare call
@@ -1055,10 +1082,12 @@ gd::String EventsCodeGenerator::GenerateBehaviorAction(
actionCode += " " + call + ";\n";
actionCode += "}\n";
if (!optionalAsyncCallbackName.empty()) {
if (!optionalAsyncCallbackName.empty() &&
!optionalAsyncCallbackId.empty()) {
actionCode +=
"runtimeScene.getAsyncTasksManager().addTask(asyncTaskGroup, " +
optionalAsyncCallbackName + ");\n };";
optionalAsyncCallbackName + ", " + optionalAsyncCallbackId +
", asyncObjectsList);\n };";
}
}
@@ -1377,17 +1406,16 @@ gd::String EventsCodeGenerator::GenerateGetVariable(
scope == VARIABLE_OR_PROPERTY_OR_PARAMETER) {
const auto variablesContainersList =
GetProjectScopedContainers().GetVariablesContainersList();
const auto &variablesContainer =
const auto& variablesContainer =
scope == VARIABLE_OR_PROPERTY_OR_PARAMETER
? variablesContainersList.GetVariablesContainerFromVariableOrPropertyOrParameterName(
variableName)
? variablesContainersList
.GetVariablesContainerFromVariableOrPropertyOrParameterName(
variableName)
: scope == VARIABLE_OR_PROPERTY
? variablesContainersList
.GetVariablesContainerFromVariableOrPropertyName(
variableName)
: variablesContainersList
.GetVariablesContainerFromVariableNameOnly(
variableName);
.GetVariablesContainerFromVariableOrPropertyName(variableName)
: variablesContainersList.GetVariablesContainerFromVariableNameOnly(
variableName);
const auto sourceType = variablesContainer.GetSourceType();
if (sourceType == gd::VariablesContainer::SourceType::Scene) {
variables = &variablesContainer;
@@ -1410,48 +1438,50 @@ gd::String EventsCodeGenerator::GenerateGetVariable(
gd::VariablesContainer::SourceType::ExtensionScene) {
variables = &variablesContainer;
output = "eventsFunctionContext.sceneVariablesForExtension";
} else if (sourceType ==
gd::VariablesContainer::SourceType::Properties) {
} else if (sourceType == gd::VariablesContainer::SourceType::Properties) {
if (hasChild) {
// Properties with children are not supported.
return "gdjs.VariablesContainer.badVariablesContainer";
}
const auto &propertiesContainersList =
const auto& propertiesContainersList =
GetProjectScopedContainers().GetPropertiesContainersList();
const auto &propertiesContainerAndProperty =
const auto& propertiesContainerAndProperty =
propertiesContainersList.Get(variableName);
return GeneratePropertyGetterWithoutCasting(
propertiesContainerAndProperty.first,
propertiesContainerAndProperty.second);
} else if (sourceType ==
gd::VariablesContainer::SourceType::Parameters) {
} else if (sourceType == gd::VariablesContainer::SourceType::Parameters) {
if (hasChild) {
// Parameters with children are not supported.
return "gdjs.VariablesContainer.badVariablesContainer";
}
const auto &parametersVectorsList =
const auto& parametersVectorsList =
GetProjectScopedContainers().GetParametersVectorsList();
const auto &parameter =
const auto& parameter =
gd::ParameterMetadataTools::Get(parametersVectorsList, variableName);
return GenerateParameterGetterWithoutCasting(parameter);
}
} else if (scope == LAYOUT_VARIABLE) {
output = "runtimeScene.getScene().getVariables()";
const auto *legacySceneVariables = GetProjectScopedContainers().GetLegacySceneVariables();
const auto* legacySceneVariables =
GetProjectScopedContainers().GetLegacySceneVariables();
if (HasProjectAndLayout()) {
variables = &GetLayout().GetVariables();
} else if (legacySceneVariables && legacySceneVariables->Has(variableName)) {
} else if (legacySceneVariables &&
legacySceneVariables->Has(variableName)) {
variables = legacySceneVariables;
output = "eventsFunctionContext.sceneVariablesForExtension";
}
} else if (scope == PROJECT_VARIABLE) {
output = "runtimeScene.getGame().getVariables()";
const auto *legacyGlobalVariables = GetProjectScopedContainers().GetLegacyGlobalVariables();
const auto* legacyGlobalVariables =
GetProjectScopedContainers().GetLegacyGlobalVariables();
if (HasProjectAndLayout()) {
variables = &GetProject().GetVariables();
} else if (legacyGlobalVariables && legacyGlobalVariables->Has(variableName)) {
} else if (legacyGlobalVariables &&
legacyGlobalVariables->Has(variableName)) {
variables = legacyGlobalVariables;
output = "eventsFunctionContext.globalVariablesForExtension";
}
@@ -1543,9 +1573,9 @@ gd::String EventsCodeGenerator::GenerateProfilerSectionEnd(
}
gd::String EventsCodeGenerator::GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode) {
bool isLocalProperty =
projectScopedContainers.GetPropertiesContainersList()
.GetBottomMostPropertiesContainer() == &propertiesContainer;
@@ -1571,8 +1601,8 @@ gd::String EventsCodeGenerator::GeneratePropertySetterWithoutCasting(
}
gd::String EventsCodeGenerator::GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property) {
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property) {
bool isLocalProperty =
projectScopedContainers.GetPropertiesContainersList()
.GetBottomMostPropertiesContainer() == &propertiesContainer;
@@ -1639,7 +1669,7 @@ gd::String EventsCodeGenerator::GeneratePropertyGetter(
}
gd::String EventsCodeGenerator::GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata &parameter) {
const gd::ParameterMetadata& parameter) {
return "eventsFunctionContext.getArgument(" +
ConvertToStringExplicit(parameter.GetName()) + ")";
}

View File

@@ -59,7 +59,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* Generate JavaScript for executing events of an events based function.
*
* \param project Project used.
* \param eventsFunctionsExtension The container of the compiled event function.
* \param eventsFunctionsExtension The container of the compiled event
* function.
* \param eventsFunction The events function to be compiled.
* \param codeNamespace Where to store the context used by the function.
* \param includeFiles Will be filled with the necessary include files.
@@ -96,7 +97,7 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
*/
static gd::String GenerateBehaviorEventsFunctionCode(
gd::Project& project,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedBehavior& eventsBasedBehavior,
const gd::EventsFunction& eventsFunction,
const gd::String& codeNamespace,
@@ -111,7 +112,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* function.
*
* \param project Project used.
* \param eventsBasedObject The object that contains the function to be compiled.
* \param eventsBasedObject The object that contains the function to be
* compiled.
* \param eventsFunction The events function to be compiled.
* \param codeNamespace Where to store the context used by the function.
* \param fullyQualifiedFunctionName The function name with its namespace.
@@ -129,7 +131,7 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
*/
static gd::String GenerateObjectEventsFunctionCode(
gd::Project& project,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedObject& eventsBasedObject,
const gd::EventsFunction& eventsFunction,
const gd::String& codeNamespace,
@@ -152,7 +154,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* \return Code
*/
virtual gd::String GenerateEventsListCode(
gd::EventsList& events, gd::EventsCodeGenerationContext& context) override;
gd::EventsList& events,
gd::EventsCodeGenerationContext& context) override;
/**
* Generate code for executing a condition list
@@ -194,7 +197,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* \brief Get the full name for accessing to a list of objects
*/
virtual gd::String GetObjectListName(
const gd::String& name, const gd::EventsCodeGenerationContext& context) override;
const gd::String& name,
const gd::EventsCodeGenerationContext& context) override;
/**
* \brief Get the namespace to be used to store code generated
@@ -226,9 +230,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
};
virtual gd::String GeneratePropertySetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property,
const gd::String &operandCode) override;
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& operandCode) override;
protected:
virtual gd::String GenerateParameterCodes(
@@ -289,7 +293,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "") override;
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "") override;
virtual gd::String GenerateBehaviorAction(
const gd::String& objectName,
@@ -299,7 +304,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
const std::vector<gd::String>& arguments,
const gd::InstructionMetadata& instrInfos,
gd::EventsCodeGenerationContext& context,
const gd::String& optionalAsyncCallbackName = "") override;
const gd::String& optionalAsyncCallbackName = "",
const gd::String& optionalAsyncCallbackId = "") override;
virtual gd::String GenerateGetBehaviorNameCode(
const gd::String& behaviorName) override;
@@ -320,9 +326,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
gd::String expressionCode) override {
// This uses `getChild` which allows to access a child
// with a number (an index, for an array) or a string (for a structure).
// This could be optimised, if the type of the accessed variable AND the type of the index is known,
// so that `getChildAt` (for an array, with an index) or `getChildNamed` (for a structure, with a name)
// is used instead.
// This could be optimised, if the type of the accessed variable AND the
// type of the index is known, so that `getChildAt` (for an array, with an
// index) or `getChildNamed` (for a structure, with a name) is used instead.
return ".getChild(" + expressionCode + ")";
};
@@ -330,29 +336,33 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
return "gdjs.VariablesContainer.badVariable";
}
virtual gd::String GeneratePropertyGetter(const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GeneratePropertyGetter(
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GeneratePropertyGetterWithoutCasting(
const gd::PropertiesContainer &propertiesContainer,
const gd::NamedPropertyDescriptor &property) override;
const gd::PropertiesContainer& propertiesContainer,
const gd::NamedPropertyDescriptor& property) override;
virtual gd::String GenerateParameterGetter(const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateParameterGetter(
const gd::ParameterMetadata& parameter,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateParameterGetterWithoutCasting(
const gd::ParameterMetadata &parameter) override;
const gd::ParameterMetadata& parameter) override;
virtual gd::String GenerateBadObject() override { return "null"; }
virtual gd::String GenerateObject(const gd::String& objectName,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateObject(
const gd::String& objectName,
const gd::String& type,
gd::EventsCodeGenerationContext& context) override;
virtual gd::String GenerateNegatedPredicate(const gd::String& predicate) const override {
virtual gd::String GenerateNegatedPredicate(
const gd::String& predicate) const override {
return "!(" + predicate + ")";
};
@@ -362,13 +372,15 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
virtual gd::String GenerateAllInstancesGetterCode(
const gd::String& objectName, gd::EventsCodeGenerationContext& context);
virtual gd::String GenerateProfilerSectionBegin(const gd::String& section) override;
virtual gd::String GenerateProfilerSectionEnd(const gd::String& section) override;
virtual gd::String GenerateProfilerSectionBegin(
const gd::String& section) override;
virtual gd::String GenerateProfilerSectionEnd(
const gd::String& section) override;
virtual gd::String GenerateRelationalOperation(
const gd::String& relationalOperator,
const gd::String& lhs,
const gd::String& rhs) override;
const gd::String& relationalOperator,
const gd::String& lhs,
const gd::String& rhs) override;
private:
static gd::String GenerateEventsListCompleteFunctionCode(
@@ -415,8 +427,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* arguments from the rest of the events.
*/
gd::String GenerateFreeEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunction &eventsFunction,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable);
/**
@@ -425,9 +437,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* arguments from the rest of the events.
*/
gd::String GenerateBehaviorEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedBehavior& eventsBasedBehavior,
const gd::EventsFunction &eventsFunction,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable,
const gd::String& thisObjectName,
const gd::String& thisBehaviorName);
@@ -438,9 +450,9 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* arguments from the rest of the events.
*/
gd::String GenerateObjectEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsBasedObject& eventsBasedObject,
const gd::EventsFunction &eventsFunction,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable,
const gd::String& thisObjectName);
@@ -455,7 +467,8 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
/**
* \brief Construct a code generator for the specified containers.
*/
EventsCodeGenerator(const gd::ProjectScopedContainers& projectScopedContainers);
EventsCodeGenerator(
const gd::ProjectScopedContainers& projectScopedContainers);
virtual ~EventsCodeGenerator();
gd::String codeNamespace; ///< Optional namespace for the generated code,
@@ -467,16 +480,16 @@ class EventsCodeGenerator : public gd::EventsCodeGenerator {
* to provides access objects, object creation and access to arguments from
* the rest of the events.
*/
gd::String GenerateEventsFunctionContext(
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
const gd::EventsFunctionsContainer &eventsFunctionsContainer,
const gd::EventsFunction &eventsFunction,
const gd::String &onceTriggersVariable,
gd::String &objectsGettersMap,
gd::String &objectArraysMap,
gd::String &behaviorNamesMap,
const gd::String &thisObjectName = "",
const gd::String &thisBehaviorName = "");
gd::String GenerateEventsFunctionContext(
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
const gd::EventsFunctionsContainer& eventsFunctionsContainer,
const gd::EventsFunction& eventsFunction,
const gd::String& onceTriggersVariable,
gd::String& objectsGettersMap,
gd::String& objectArraysMap,
gd::String& behaviorNamesMap,
const gd::String& thisObjectName = "",
const gd::String& thisBehaviorName = "");
};
} // namespace gdjs

View File

@@ -22,9 +22,11 @@ AsyncExtension::AsyncExtension() {
gd::AsyncEvent &event = dynamic_cast<gd::AsyncEvent &>(event_);
// Generate callback code
const auto callbackDescriptor = codeGenerator.GenerateCallback(
const auto callbackId =
gd::String::From(codeGenerator.GenerateSingleUsageUniqueIdFor(
event.GetInstruction().GetOriginalInstruction().lock().get())),
event.GetInstruction().GetOriginalInstruction().lock().get()));
const auto callbackDescriptor = codeGenerator.GenerateCallback(
callbackId,
parentContext,
event.GetActions(),
event.HasSubEvents() ? &event.GetSubEvents() : nullptr);
@@ -33,10 +35,6 @@ AsyncExtension::AsyncExtension() {
"(runtimeScene) => (" + callbackDescriptor.functionName + "(" +
callbackDescriptor.argumentsList + "))";
// Generate the action and store the generated task.
const gd::String asyncActionCode = codeGenerator.GenerateActionCode(
event.GetInstruction(), parentContext, callbackCallCode);
// Generate code to backup the objects lists.
// Do it after generating the code of the action so that it uses the
// same object list as used in the action.
@@ -70,6 +68,13 @@ AsyncExtension::AsyncExtension() {
", obj);\n";
}
// Generate the action and store the generated task.
const gd::String asyncActionCode =
codeGenerator.GenerateActionCode(event.GetInstruction(),
parentContext,
callbackCallCode,
callbackId);
return "{\n" + parentAsyncObjectsListGetter + "{\n" +
asyncContextBuilder + asyncActionCode + "}\n" + "}\n";
});

View File

@@ -53,6 +53,7 @@ BaseObjectExtension::BaseObjectExtension() {
"runtimeobject.js");
objectActions["RotateTowardAngle"].SetFunctionName("rotateTowardAngle");
objectActions["RotateTowardPosition"].SetFunctionName("rotateTowardPosition");
objectActions["RotateTowardObject"].SetFunctionName("rotateTowardObject");
objectActions["ChangeLayer"].SetFunctionName("setLayer");
objectConditions["Layer"].SetFunctionName("isOnLayer");
objectActions["ChangePlan"]
@@ -229,6 +230,8 @@ BaseObjectExtension::BaseObjectExtension() {
"gdjs.evtTools.object.pickRandomObject");
GetAllConditions()["AjoutHasard"].SetFunctionName(
"gdjs.evtTools.object.pickRandomObject");
GetAllActions()["PickNearest"].SetFunctionName(
"gdjs.evtTools.object.pickNearestObject");
GetAllConditions()["PickNearest"].SetFunctionName(
"gdjs.evtTools.object.pickNearestObject");

View File

@@ -137,7 +137,8 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
//...and export it
gd::SerializerElement noRuntimeGameOptions;
helper.ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
noRuntimeGameOptions);
noRuntimeGameOptions,
/*isInGameEdition=*/false);
includesFiles.push_back(codeOutputDir + "/data.js");
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
@@ -203,8 +204,7 @@ bool Exporter::ExportWholePixiProject(const ExportOptions &options) {
void Exporter::SerializeProjectData(const gd::Project &project,
const PreviewExportOptions &options,
gd::SerializerElement &projectDataElement) {
ExporterHelper::SerializeProjectData(fs, project, options,
projectDataElement);
ExporterHelper::SerializeProjectData(fs, project, options, projectDataElement);
}
void Exporter::SerializeRuntimeGameOptions(

View File

@@ -286,10 +286,8 @@ bool ExporterHelper::ExportProjectForPixiPreview(
gd::SerializerElement runtimeGameOptions;
ExporterHelper::SerializeRuntimeGameOptions(fs, gdjsRoot, options,
includesFiles, runtimeGameOptions);
ExportProjectData(fs,
exportedProject,
codeOutputDir + "/data.js",
runtimeGameOptions);
ExportProjectData(fs, exportedProject, codeOutputDir + "/data.js",
runtimeGameOptions, options.isInGameEdition);
includesFiles.push_back(codeOutputDir + "/data.js");
previousTime = LogTimeSpent("Project data export", previousTime);
@@ -299,6 +297,9 @@ bool ExporterHelper::ExportProjectForPixiPreview(
}
if (options.shouldReloadLibraries) {
if (options.isInGameEdition) {
InsertUnique(resourcesFiles, "InGameEditor/Resources/primitivedrawingicon.png");
}
// Copy all the dependencies and their source maps
ExportIncludesAndLibs(includesFiles, options.exportPath, true);
ExportIncludesAndLibs(resourcesFiles, options.exportPath, true);
@@ -324,11 +325,12 @@ bool ExporterHelper::ExportProjectForPixiPreview(
gd::String ExporterHelper::ExportProjectData(
gd::AbstractFileSystem &fs, gd::Project &project, gd::String filename,
const gd::SerializerElement &runtimeGameOptions) {
const gd::SerializerElement &runtimeGameOptions, bool isInGameEdition) {
fs.MkDir(fs.DirNameFrom(filename));
gd::SerializerElement projectDataElement;
ExporterHelper::StriptAndSerializeProjectData(project, projectDataElement);
ExporterHelper::StriptAndSerializeProjectData(
project, projectDataElement, isInGameEdition);
// Save the project to JSON
gd::String output =
@@ -344,8 +346,7 @@ gd::String ExporterHelper::ExportProjectData(
void ExporterHelper::SerializeRuntimeGameOptions(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
const PreviewExportOptions &options,
std::vector<gd::String> &includesFiles,
const PreviewExportOptions &options, std::vector<gd::String> &includesFiles,
gd::SerializerElement &runtimeGameOptions) {
// Create the setup options passed to the gdjs.RuntimeGame
runtimeGameOptions.AddChild("isPreview").SetBoolValue(true);
@@ -477,6 +478,14 @@ void ExporterHelper::SerializeRuntimeGameOptions(
}
}
void ExporterHelper::AddInGameEditorResources(
gd::Project &project, std::set<gd::String> &projectUsedResources) {
project.GetResourcesManager().AddResource(
"InGameEditor-ShapePainterIcon",
"InGameEditor/Resources/primitivedrawingicon.png", "image");
projectUsedResources.insert("InGameEditor-ShapePainterIcon");
}
void ExporterHelper::SerializeProjectData(gd::AbstractFileSystem &fs,
const gd::Project &project,
const PreviewExportOptions &options,
@@ -497,13 +506,18 @@ void ExporterHelper::SerializeProjectData(gd::AbstractFileSystem &fs,
gd::ResourceExposer::ExposeWholeProjectResources(clonedProject,
resourcesMergingHelper);
ExporterHelper::StriptAndSerializeProjectData(clonedProject, rootElement);
ExporterHelper::StriptAndSerializeProjectData(
clonedProject, rootElement, options.isInGameEdition);
}
void ExporterHelper::StriptAndSerializeProjectData(
gd::Project &project, gd::SerializerElement &rootElement) {
gd::Project &project, gd::SerializerElement &rootElement,
bool isInGameEdition) {
auto projectUsedResources =
gd::SceneResourcesFinder::FindProjectResources(project);
if (isInGameEdition) {
ExporterHelper::AddInGameEditorResources(project, projectUsedResources);
}
std::unordered_map<gd::String, std::set<gd::String>> scenesUsedResources;
for (std::size_t layoutIndex = 0;
layoutIndex < project.GetLayoutsCount(); layoutIndex++) {
@@ -1073,6 +1087,7 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "CustomRuntimeObjectInstanceContainer.js");
InsertUnique(includesFiles, "CustomRuntimeObject.js");
InsertUnique(includesFiles, "CustomRuntimeObject2D.js");
InsertUnique(includesFiles, "indexeddb.js");
// Common includes for events only.
InsertUnique(includesFiles, "events-tools/commontools.js");

View File

@@ -481,16 +481,16 @@ class ExporterHelper {
* \return Empty string if everything is ok,
* description of the error otherwise.
*/
static gd::String
ExportProjectData(gd::AbstractFileSystem &fs, gd::Project &project,
gd::String filename,
const gd::SerializerElement &runtimeGameOptions);
static gd::String ExportProjectData(
gd::AbstractFileSystem &fs, gd::Project &project, gd::String filename,
const gd::SerializerElement &runtimeGameOptions, bool isInGameEdition);
/**
* \brief Serialize a project without its events to JSON
*
* \param fs The abstract file system to use to write the file
* \param project The project to be exported.
* \param options The content of the extra configuration
* \param projectDataElement The element where the project data is serialized
*/
static void SerializeProjectData(gd::AbstractFileSystem &fs,
@@ -503,6 +503,7 @@ class ExporterHelper {
* in gdjs.runtimeGameOptions to JSON
*
* \param fs The abstract file system to use to write the file
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
* \param options The content of the extra configuration
* \param includesFiles The list of scripts files - useful for hot-reloading
* \param runtimeGameOptionsElement The element where the game options are
@@ -705,6 +706,9 @@ class ExporterHelper {
/**
* \brief Given an include file, returns the name of the file to reference
* in the exported game.
*
* \param fs The abstract file system to use
* \param gdjsRoot The root directory of GDJS, used to copy runtime files.
*/
static gd::String GetExportedIncludeFilename(
gd::AbstractFileSystem &fs, const gd::String &gdjsRoot,
@@ -747,9 +751,23 @@ class ExporterHelper {
*
* \param project The project to be exported.
*/
static void StriptAndSerializeProjectData(gd::Project &project,
gd::SerializerElement &rootElement,
bool isInGameEdition);
/**
* \brief Add additional resources that are used by the in-game editor to the
* project.
*
* \param project The project to be exported where resource declarations are
* added.
*
* \param projectUsedResources The list of resource to be loaded
* globally by the runtime.
*/
static void
StriptAndSerializeProjectData(gd::Project &project,
gd::SerializerElement &rootElement);
AddInGameEditorResources(gd::Project &project,
std::set<gd::String> &projectUsedResources);
};
} // namespace gdjs

View File

@@ -4,6 +4,33 @@
* This project is released under the MIT License.
*/
namespace gdjs {
export type WaitTaskNetworkSyncData = {
type: 'wait';
duration: float;
timeElapsedOnScene: float;
};
export type ResolveTaskNetworkSyncData = null;
export type PromiseTaskNetworkSyncData = null;
export type ManuallyResolvableTaskNetworkSyncData = null;
export type TaskGroupNetworkSyncData = {
type: 'group';
tasks: AsyncTaskNetworkSyncData[];
};
export type AsyncTaskNetworkSyncData =
| WaitTaskNetworkSyncData
| TaskGroupNetworkSyncData
| PromiseTaskNetworkSyncData
| ManuallyResolvableTaskNetworkSyncData
| ResolveTaskNetworkSyncData;
export type AsyncTasksManagerNetworkSyncData = {
tasks: Array<{
callbackId: string;
asyncTask: AsyncTaskNetworkSyncData;
objectsList: gdjs.LongLivedObjectsListNetworkSyncData;
}>;
};
/**
* This stores all asynchronous tasks waiting to be completed,
* for a given scene.
@@ -15,7 +42,12 @@ namespace gdjs {
*/
private tasksWithCallback = new Array<{
asyncTask: AsyncTask;
callback: (runtimeScene: gdjs.RuntimeScene) => void;
callback: (
runtimeScene: gdjs.RuntimeScene,
longLivedObjectsList: gdjs.LongLivedObjectsList
) => void;
callbackId: string;
longLivedObjectsList: gdjs.LongLivedObjectsList;
}>();
/**
@@ -26,7 +58,10 @@ namespace gdjs {
const taskWithCallback = this.tasksWithCallback[i];
if (taskWithCallback.asyncTask.update(runtimeScene)) {
// The task has finished, run the callback and remove it.
taskWithCallback.callback(runtimeScene);
taskWithCallback.callback(
runtimeScene,
taskWithCallback.longLivedObjectsList
);
this.tasksWithCallback.splice(i--, 1);
}
}
@@ -39,9 +74,19 @@ namespace gdjs {
*/
addTask(
asyncTask: AsyncTask,
callback: (runtimeScene: RuntimeScene) => void
callback: (
runtimeScene: RuntimeScene,
longLivedObjectsList: gdjs.LongLivedObjectsList
) => void,
callbackId: string,
longLivedObjectsList: gdjs.LongLivedObjectsList
): void {
this.tasksWithCallback.push({ asyncTask, callback });
this.tasksWithCallback.push({
asyncTask,
callback,
callbackId,
longLivedObjectsList,
});
}
/**
@@ -51,6 +96,77 @@ namespace gdjs {
clearTasks() {
this.tasksWithCallback.length = 0;
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): AsyncTasksManagerNetworkSyncData {
const tasksData = this.tasksWithCallback.map(
({ asyncTask, callbackId, longLivedObjectsList }) => {
return {
callbackId,
asyncTask: asyncTask.getNetworkSyncData(),
objectsList: longLivedObjectsList.getNetworkSyncData(syncOptions),
};
}
);
return {
tasks: tasksData,
};
}
updateFromNetworkSyncData(
syncData: AsyncTasksManagerNetworkSyncData,
idToCallbackMap: Map<
string,
(
runtimeScene: gdjs.RuntimeScene,
asyncObjectsList: gdjs.LongLivedObjectsList
) => void
>,
runtimeScene: gdjs.RuntimeScene,
syncOptions: UpdateFromNetworkSyncDataOptions
) {
this.clearTasks();
const unknownTaskTypes: string[] = [];
syncData.tasks.forEach(({ callbackId, asyncTask, objectsList }) => {
if (!asyncTask) return;
const callback = idToCallbackMap.get(callbackId);
if (callback) {
const longLivedObjectsList = new gdjs.LongLivedObjectsList();
longLivedObjectsList.updateFromNetworkSyncData(
objectsList,
runtimeScene,
syncOptions
);
if (asyncTask.type === 'group') {
const task = new TaskGroup();
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task, callback, callbackId, longLivedObjectsList);
} else if (asyncTask.type === 'wait') {
const task = new gdjs.evtTools.runtimeScene.WaitTask(
asyncTask.duration
);
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task, callback, callbackId, longLivedObjectsList);
} else {
// Unknown task type.
// @ts-ignore
unknownTaskTypes.push(asyncTask.type);
}
}
});
if (unknownTaskTypes.length) {
console.warn(
`${unknownTaskTypes.length} asynchronous task(s) could not be restored from network sync data. ${unknownTaskTypes.join(', ')}`
);
}
}
}
/**
@@ -63,6 +179,12 @@ namespace gdjs {
* @return True if the task is finished, false if it needs to continue running.
*/
abstract update(runtimeScene: RuntimeScene): boolean;
abstract getNetworkSyncData(): AsyncTaskNetworkSyncData;
abstract updateFromNetworkSyncData(
syncData: AsyncTaskNetworkSyncData
): void;
}
export class TaskGroup extends AsyncTask {
@@ -80,12 +202,54 @@ namespace gdjs {
return this.tasks.length === 0;
}
getNetworkSyncData(): TaskGroupNetworkSyncData {
return {
type: 'group',
tasks: this.tasks.map((task) => task.getNetworkSyncData()),
};
}
updateFromNetworkSyncData(syncData: TaskGroupNetworkSyncData) {
const unknownTaskTypes: string[] = [];
syncData.tasks.forEach((asyncTask) => {
if (!asyncTask) return;
if (asyncTask.type === 'group') {
const task = new TaskGroup();
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task);
} else if (asyncTask.type === 'wait') {
const task = new gdjs.evtTools.runtimeScene.WaitTask(
asyncTask.duration
);
task.updateFromNetworkSyncData(asyncTask);
this.addTask(task);
} else {
// Unknown task type.
// @ts-ignore
unknownTaskTypes.push(asyncTask.type);
}
});
if (unknownTaskTypes.length) {
console.warn(
`${unknownTaskTypes.length} asynchronous task(s) could not be restored from network sync data. ${unknownTaskTypes.join(', ')}`
);
}
}
}
export class ResolveTask extends AsyncTask {
update() {
return true;
}
getNetworkSyncData(): AsyncTaskNetworkSyncData {
return null;
}
updateFromNetworkSyncData(syncData: AsyncTaskNetworkSyncData): void {}
}
const logger = new gdjs.Logger('Internal PromiseTask');
@@ -121,6 +285,12 @@ ${error ? 'The following error was thrown: ' + error : ''}`
update() {
return this.isResolved;
}
getNetworkSyncData(): AsyncTaskNetworkSyncData {
return null;
}
updateFromNetworkSyncData(syncData: AsyncTaskNetworkSyncData): void {}
}
export class ManuallyResolvableTask extends AsyncTask {
@@ -133,5 +303,11 @@ ${error ? 'The following error was thrown: ' + error : ''}`
update(): boolean {
return this.isResolved;
}
getNetworkSyncData(): AsyncTaskNetworkSyncData {
return null;
}
updateFromNetworkSyncData(syncData: AsyncTaskNetworkSyncData): void {}
}
}

View File

@@ -17,11 +17,19 @@ namespace gdjs {
isInnerAreaFollowingParentSize: boolean;
};
export type CustomObjectNetworkSyncDataType = ObjectNetworkSyncData & {
export type CustomObjectNetworkSyncDataType = {
anim?: SpriteAnimatorNetworkSyncData;
ifx: boolean;
ify: boolean;
sx: float;
sy: float;
op: float;
cc?: [float, float];
};
export type CustomObjectNetworkSyncData = ObjectNetworkSyncData &
CustomObjectNetworkSyncDataType;
/**
* An object that contains other object.
*
@@ -209,24 +217,66 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): CustomObjectNetworkSyncDataType {
return {
...super.getNetworkSyncData(),
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): CustomObjectNetworkSyncData {
const animator = this.getAnimator();
const networkSyncData: CustomObjectNetworkSyncData = {
...super.getNetworkSyncData(syncOptions),
ifx: this.isFlippedX(),
ify: this.isFlippedY(),
sx: this._scaleX,
sy: this._scaleY,
op: this.opacity,
};
if (animator) {
networkSyncData.anim = animator.getNetworkSyncData();
}
if (this._customCenter) {
networkSyncData.cc = this._customCenter;
}
return networkSyncData;
}
updateFromNetworkSyncData(
networkSyncData: CustomObjectNetworkSyncDataType
networkSyncData: CustomObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(networkSyncData);
super.updateFromNetworkSyncData(networkSyncData, options);
if (networkSyncData.ifx !== undefined) {
this.flipX(networkSyncData.ifx);
}
if (networkSyncData.ify !== undefined) {
this.flipY(networkSyncData.ify);
}
if (networkSyncData.sx !== undefined) {
this.setScaleX(Math.abs(networkSyncData.sx));
}
if (networkSyncData.sy !== undefined) {
this.setScaleY(Math.abs(networkSyncData.sy));
}
if (networkSyncData.op !== undefined) {
this.setOpacity(networkSyncData.op);
}
if (networkSyncData.anim) {
const animator = this.getAnimator();
if (animator) {
animator.updateFromNetworkSyncData(networkSyncData.anim);
}
}
if (networkSyncData.cc) {
this.setRotationCenter(networkSyncData.cc[0], networkSyncData.cc[1]);
}
if (
networkSyncData.ifx !== undefined ||
networkSyncData.ify !== undefined ||
networkSyncData.sx !== undefined ||
networkSyncData.sy !== undefined ||
networkSyncData.anim !== undefined ||
networkSyncData.cc !== undefined
) {
this.onChildrenLocationChanged();
}
}
override extraInitializationFromInitialInstance(

View File

@@ -78,19 +78,23 @@ namespace gdjs {
return value !== null && value !== undefined;
}
type Point3D = [float, float, float];
type RuntimeObjectWith3D = RuntimeObject &
Base3DHandler &
Resizable &
Scalable &
Flippable;
Flippable & {
getCenterZInScene(): float;
};
const is3D = (object: gdjs.RuntimeObject): object is RuntimeObjectWith3D => {
return gdjs.Base3DHandler.is3D(object);
};
type AABB3D = {
min: [float, float, float];
max: [float, float, float];
min: Point3D;
max: Point3D;
};
const defaultEffectsData: EffectData[] = [
@@ -410,18 +414,41 @@ namespace gdjs {
}
object.setX(Math.round(initialPosition.x + movement.translationX));
object.setY(Math.round(initialPosition.y + movement.translationY));
object.setAngle(Math.round(initialPosition.angle + movement.rotationZ));
object.setWidth(initialPosition.width * Math.abs(movement.scaleX));
object.setHeight(initialPosition.height * Math.abs(movement.scaleY));
object.setAngle(
gdjs.evtTools.common.mod(
Math.round(initialPosition.angle + movement.rotationZ),
360
)
);
if (movement.scaleX !== 1) {
object.setWidth(
Math.round(initialPosition.width * Math.abs(movement.scaleX))
);
}
if (movement.scaleY !== 1) {
object.setHeight(
Math.round(initialPosition.height * Math.abs(movement.scaleY))
);
}
if (is3D(object)) {
object.setZ(Math.round(initialPosition.z + movement.translationZ));
object.setRotationX(
Math.round(initialPosition.rotationX + movement.rotationX)
gdjs.evtTools.common.mod(
Math.round(initialPosition.rotationX + movement.rotationX),
360
)
);
object.setRotationY(
Math.round(initialPosition.rotationY + movement.rotationY)
gdjs.evtTools.common.mod(
Math.round(initialPosition.rotationY + movement.rotationY),
360
)
);
object.setDepth(initialPosition.depth * Math.abs(movement.scaleZ));
if (movement.scaleZ !== 1) {
object.setDepth(
Math.round(initialPosition.depth * Math.abs(movement.scaleZ))
);
}
}
});
}
@@ -465,6 +492,7 @@ namespace gdjs {
private _isVisible = true;
private _timeSinceLastInteraction = 0;
private _isFirstFrame = true;
private _editorCamera;
@@ -490,6 +518,7 @@ namespace gdjs {
scaleZ: float;
} | null = null;
private _hasSelectionActuallyMoved = false;
private _isTransformControlsHovered = false;
private _wasMovingSelectionLastFrame = false;
private _selectionBox: THREE_ADDONS.SelectionBox | null = null;
@@ -509,6 +538,8 @@ namespace gdjs {
private _wasMouseLeftButtonPressed = false;
private _pressedOriginalCursorX: float = 0;
private _pressedOriginalCursorY: float = 0;
private _previousCursorX: float = 0;
private _previousCursorY: float = 0;
// Dragged new object:
private _draggedNewObject: gdjs.RuntimeObject | null = null;
@@ -597,6 +628,10 @@ namespace gdjs {
return this._currentScene;
}
areEffectsHidden(): boolean {
return !!this._runtimeGame.getGameData().areEffectsHiddenInEditor;
}
/**
* Return the layer to be used for camera calculus.
* @see getEditorLayer
@@ -770,6 +805,7 @@ namespace gdjs {
// Try to keep object selection in case the same scene is reloaded.
this.setSelectedObjects(selectedObjectIds);
this._isFirstFrame = true;
}
private _createSceneWithCustomObject(
@@ -1137,8 +1173,6 @@ namespace gdjs {
}
this._getEditorCamera().switchToOrbitAroundObject(object);
// We keep the same camera distance.
this._getEditorCamera().resetRotationToTopDown();
}
private _handleCameraMovement() {
@@ -1209,7 +1243,7 @@ namespace gdjs {
private _shouldDragSelectedObject(): boolean {
const inputManager = this._runtimeGame.getInputManager();
return (
(isAltPressed(inputManager) || isControlOrCmdPressed(inputManager)) &&
isControlOrCmdPressed(inputManager) &&
(!this._selectionControls ||
!this._selectionControls.threeTransformControls.dragging)
);
@@ -1259,21 +1293,37 @@ namespace gdjs {
this._selection.getSelectedObjects()
);
if (cursor) {
const [cursorX, cursorY, cursorZ] = cursor;
isIntersectionFound = true;
intersectionX = cursorX;
intersectionY = cursorY;
intersectionZ = cursorZ;
[intersectionX, intersectionY, intersectionZ] = cursor;
}
} else {
const projectedCursor = this._getProjectedCursor();
if (projectedCursor) {
isIntersectionFound = true;
intersectionX = projectedCursor[0];
intersectionY = projectedCursor[1];
[intersectionX, intersectionY] = projectedCursor;
}
}
if (isIntersectionFound) {
this._editorGrid.setNormal('Z');
this._editorGrid.setPosition(
intersectionX,
intersectionY,
intersectionZ
);
const cameraLayer = this.getCameraLayer(
this._draggedSelectedObject.getLayer()
);
const threeScene = cameraLayer
? cameraLayer.getRenderer().getThreeScene()
: null;
if (threeScene) {
this._editorGrid.setTreeScene(threeScene);
}
this._editorGrid.setVisible(true);
if (this._editorGrid.isSpanningEnabled(inputManager)) {
intersectionX = this._editorGrid.getSnappedX(intersectionX);
intersectionY = this._editorGrid.getSnappedY(intersectionY);
}
this._draggedSelectedObjectTotalDelta.translationX =
intersectionX - this._draggedSelectedObjectInitialX;
this._draggedSelectedObjectTotalDelta.translationY =
@@ -1487,6 +1537,10 @@ namespace gdjs {
const editedInstanceContainer = this.getEditedInstanceContainer();
if (!editedInstanceContainer) return;
if (this._isTransformControlsHovered) {
return;
}
const inputManager = this._runtimeGame.getInputManager();
if (inputManager.wasKeyJustPressed(ESC_KEY)) {
@@ -1561,7 +1615,8 @@ namespace gdjs {
// Remove boxes for deselected objects
this._selectionBoxes.forEach(({ container, box }, object) => {
const isHovered = object === objectUnderCursor;
const isHovered =
object === objectUnderCursor && !this._isTransformControlsHovered;
const isInSelection = selected3DObjects.includes(object);
if (!isInSelection && !isHovered) {
container.removeFromParent();
@@ -1659,7 +1714,6 @@ namespace gdjs {
dummyThreeObject.rotation.y = -dummyThreeObject.rotation.y;
dummyThreeObject.rotation.z = -dummyThreeObject.rotation.z;
}
this._editorGrid.setVisible(threeTransformControls.mode === 'translate');
}
private _forceUpdateSelectionControls() {
@@ -1712,160 +1766,280 @@ namespace gdjs {
lastEditableSelectedObject &&
!this._selectionControls &&
!this._shouldDragSelectedObject() &&
!shouldHideSelectionControls
!shouldHideSelectionControls &&
lastEditableSelectedObject.get3DRendererObject()
) {
const threeObject = lastEditableSelectedObject.get3DRendererObject();
if (!threeObject) return;
const cameraLayer = this.getCameraLayer(
lastEditableSelectedObject.getLayer()
);
if (!cameraLayer) return;
if (cameraLayer) {
const runtimeLayerRender = cameraLayer
? cameraLayer.getRenderer()
: null;
const threeCamera = runtimeLayerRender
? runtimeLayerRender.getThreeCamera()
: null;
const threeScene = runtimeLayerRender
? runtimeLayerRender.getThreeScene()
: null;
if (threeCamera && threeScene) {
// Create and attach the transform controls. It is attached to a dummy object
// to avoid the controls to directly move the runtime object (we handle this
// manually).
const threeTransformControls = new THREE_ADDONS.TransformControls(
threeCamera,
this._runtimeGame.getRenderer().getCanvas() || undefined
);
threeTransformControls.rotation.order = 'ZYX';
threeTransformControls.scale.y = -1;
threeTransformControls.traverse((obj) => {
// To be detected correctly by OutlinePass.
// @ts-ignore
obj.isTransformControls = true;
});
const runtimeLayerRender = cameraLayer.getRenderer();
const threeCamera = runtimeLayerRender.getThreeCamera();
const threeScene = runtimeLayerRender.getThreeScene();
if (!threeCamera || !threeScene) return;
// The dummy object is an invisible object that is the one moved by the transform
// controls.
const dummyThreeObject = new THREE.Object3D();
this._updateDummyLocation(
dummyThreeObject,
lastEditableSelectedObject,
threeTransformControls
);
threeScene.add(dummyThreeObject);
// Create and attach the transform controls. It is attached to a dummy object
// to avoid the controls to directly move the runtime object (we handle this
// manually).
const threeTransformControls = new THREE_ADDONS.TransformControls(
threeCamera,
this._runtimeGame.getRenderer().getCanvas() || undefined
);
threeTransformControls.rotation.order = 'ZYX';
threeTransformControls.scale.y = -1;
threeTransformControls.traverse((obj) => {
// To be detected correctly by OutlinePass.
// @ts-ignore
obj.isTransformControls = true;
});
threeTransformControls.attach(dummyThreeObject);
threeScene.add(threeTransformControls);
this._editorGrid.setNormal('Z');
this._editorGrid.setPosition(
threeObject.position.x,
threeObject.position.y,
threeObject.position.z
);
this._editorGrid.setTreeScene(threeScene);
this._editorGrid.setVisible(true);
// Keep track of the movement so the editor can apply it to the selection.
let initialObjectX = 0;
let initialObjectY = 0;
let initialObjectZ = 0;
const initialDummyPosition = new THREE.Vector3();
const initialDummyRotation = new THREE.Euler();
const initialDummyScale = new THREE.Vector3();
threeTransformControls.addEventListener('change', (e) => {
if (!threeTransformControls.dragging) {
this._selectionControlsMovementTotalDelta = null;
// The dummy object is an invisible object that is the one moved by the transform
// controls.
const dummyThreeObject = new THREE.Object3D();
dummyThreeObject.position.copy(threeObject.position);
dummyThreeObject.rotation.copy(threeObject.rotation);
dummyThreeObject.scale.copy(threeObject.scale);
threeScene.add(dummyThreeObject);
threeTransformControls.attach(dummyThreeObject);
threeScene.add(threeTransformControls);
// Keep track of the movement so the editor can apply it to the selection.
const initialPosition = new THREE.Vector3();
const initialRotation = new THREE.Euler();
const initialScale = new THREE.Vector3();
threeTransformControls.addEventListener('change', (e) => {
if (!threeTransformControls.dragging) {
this._selectionControlsMovementTotalDelta = null;
// Reset the initial position to the current position, so that
// it's ready to be dragged again.
initialPosition.copy(dummyThreeObject.position);
initialRotation.copy(dummyThreeObject.rotation);
initialScale.copy(dummyThreeObject.scale);
return;
}
let gridNormal: 'X' | 'Y' | 'Z' = 'Z';
let targetPositionX = dummyThreeObject.position.x;
let targetPositionY = dummyThreeObject.position.y;
let targetPositionZ = dummyThreeObject.position.z;
if (
threeTransformControls.mode === 'translate' &&
threeTransformControls.axis
) {
const isMovingOnX = threeTransformControls.axis.includes('X');
const isMovingOnY = threeTransformControls.axis.includes('Y');
const isMovingOnZ = threeTransformControls.axis.includes('Z');
if (this._editorGrid.isSpanningEnabled(inputManager)) {
if (isMovingOnX) {
targetPositionX = this._editorGrid.getSnappedX(targetPositionX);
}
if (isMovingOnY) {
targetPositionY = this._editorGrid.getSnappedY(targetPositionY);
}
if (isMovingOnZ) {
targetPositionZ = this._editorGrid.getSnappedZ(targetPositionZ);
}
}
if (isMovingOnZ) {
if (!isMovingOnX && !isMovingOnY) {
// Choose the plan that faces the camera.
const cameraRotation = Math.abs(
gdjs.evtTools.common.angleDifference(
this._editorCamera.getCameraRotation(),
0
)
this._updateDummyLocation(
dummyThreeObject,
lastEditableSelectedObject,
threeTransformControls
);
if (cameraRotation <= 45 || cameraRotation > 135) {
gridNormal = 'Y';
} else {
gridNormal = 'X';
}
} else if (!isMovingOnX) {
gridNormal = 'X';
} else if (!isMovingOnY) {
gridNormal = 'Y';
// Reset the initial position to the current position, so that
// it's ready to be dragged again.
initialObjectX = lastEditableSelectedObject.getX();
initialObjectY = lastEditableSelectedObject.getY();
initialObjectZ = is3D(lastEditableSelectedObject)
? lastEditableSelectedObject.getZ()
: 0;
initialDummyPosition.copy(dummyThreeObject.position);
initialDummyRotation.copy(dummyThreeObject.rotation);
initialDummyScale.copy(dummyThreeObject.scale);
return;
}
let translationX =
dummyThreeObject.position.x - initialDummyPosition.x;
let translationY =
dummyThreeObject.position.y - initialDummyPosition.y;
let translationZ =
dummyThreeObject.position.z - initialDummyPosition.z;
if (
threeTransformControls.mode === 'translate' &&
threeTransformControls.axis
) {
if (threeTransformControls.axis === 'XYZ') {
// We need to override the translation vector because
// `threeTransformControls` don't know that the selection
// must be excluded when looking for the cursor position.
let isIntersectionFound = false;
let intersectionX: float = 0;
let intersectionY: float = 0;
let intersectionZ: float = 0;
if (is3D(lastEditableSelectedObject)) {
const cursor = this._getCursorIn3D(
this._selection.getSelectedObjects()
);
if (cursor) {
isIntersectionFound = true;
[intersectionX, intersectionY, intersectionZ] = cursor;
}
} else {
const projectedCursor = this._getProjectedCursor();
if (projectedCursor) {
isIntersectionFound = true;
[intersectionX, intersectionY] = projectedCursor;
}
}
if (isIntersectionFound) {
translationX = intersectionX - initialObjectX;
translationY = intersectionY - initialObjectY;
translationZ = intersectionZ - initialObjectZ;
} else {
translationX = 0;
translationY = 0;
translationZ = 0;
}
}
const isMovingOnX = threeTransformControls.axis.includes('X');
const isMovingOnY = threeTransformControls.axis.includes('Y');
const isMovingOnZ = threeTransformControls.axis.includes('Z');
if (this._editorGrid.isSpanningEnabled(inputManager)) {
if (isMovingOnX) {
translationX =
this._editorGrid.getSnappedX(
initialObjectX + translationX
) - initialObjectX;
}
if (isMovingOnY) {
translationY =
this._editorGrid.getSnappedY(
initialObjectY + translationY
) - initialObjectY;
}
if (isMovingOnZ) {
translationZ =
this._editorGrid.getSnappedZ(
initialObjectZ + translationZ
) - initialObjectZ;
}
}
}
const scaleDamping = 0.2; // 0.2 = 20% of the movement speed (Three.js transform controls scaling is too fast)
this._selectionControlsMovementTotalDelta = {
translationX,
translationY,
translationZ,
rotationX: gdjs.toDegrees(
dummyThreeObject.rotation.x - initialDummyRotation.x
),
rotationY: -gdjs.toDegrees(
dummyThreeObject.rotation.y - initialDummyRotation.y
),
rotationZ: -gdjs.toDegrees(
dummyThreeObject.rotation.z - initialDummyRotation.z
),
scaleX:
1 +
(dummyThreeObject.scale.x / initialDummyScale.x - 1) *
scaleDamping,
scaleY:
1 +
(dummyThreeObject.scale.y / initialDummyScale.y - 1) *
scaleDamping,
scaleZ:
1 +
(dummyThreeObject.scale.z / initialDummyScale.z - 1) *
scaleDamping,
};
this._hasSelectionActuallyMoved =
this._hasSelectionActuallyMoved ||
!dummyThreeObject.position.equals(initialDummyPosition) ||
!dummyThreeObject.rotation.equals(initialDummyRotation) ||
!dummyThreeObject.scale.equals(initialDummyScale);
});
this._selectionControls = {
object: lastEditableSelectedObject,
dummyThreeObject,
threeTransformControls,
};
}
}
}
if (
lastEditableSelectedObject &&
this._selectionControls &&
!this._draggedNewObject &&
!this._draggedSelectedObject
) {
const { threeTransformControls } = this._selectionControls;
const axis = threeTransformControls.axis;
if (axis) {
const isMovingOnX = axis ? axis.includes('X') : false;
const isMovingOnY = axis ? axis.includes('Y') : false;
const isMovingOnZ = axis ? axis.includes('Z') : false;
let gridNormal: 'X' | 'Y' | 'Z' = 'Z';
if (isMovingOnZ) {
if (!isMovingOnX && !isMovingOnY) {
// Choose the plan that faces the camera.
const cameraRotation = Math.abs(
gdjs.evtTools.common.angleDifference(
this._editorCamera.getCameraRotation(),
0
)
);
if (cameraRotation <= 45 || cameraRotation > 135) {
gridNormal = 'Y';
} else {
gridNormal = 'X';
}
} else if (!isMovingOnX) {
gridNormal = 'X';
} else if (!isMovingOnY) {
gridNormal = 'Y';
}
}
this._editorGrid.setNormal(gridNormal);
this._editorGrid.setPosition(
threeObject.position.x,
threeObject.position.y,
threeObject.position.z
);
}
this._editorGrid.setPosition(
lastEditableSelectedObject.getX(),
lastEditableSelectedObject.getY(),
is3D(lastEditableSelectedObject)
? lastEditableSelectedObject.getZ()
: 0
);
const cameraLayer = this.getCameraLayer(
lastEditableSelectedObject.getLayer()
);
const threeScene = cameraLayer
? cameraLayer.getRenderer().getThreeScene()
: null;
if (threeScene) {
this._editorGrid.setTreeScene(threeScene);
}
this._editorGrid.setVisible(
threeTransformControls.mode === 'translate'
);
}
}
const scaleDamping = 0.2; // 0.2 = 20% of the movement speed (Three.js transform controls scaling is too fast)
this._selectionControlsMovementTotalDelta = {
translationX: targetPositionX - initialPosition.x,
translationY: targetPositionY - initialPosition.y,
translationZ: targetPositionZ - initialPosition.z,
rotationX: gdjs.toDegrees(
dummyThreeObject.rotation.x - initialRotation.x
),
rotationY: -gdjs.toDegrees(
dummyThreeObject.rotation.y - initialRotation.y
),
rotationZ: -gdjs.toDegrees(
dummyThreeObject.rotation.z - initialRotation.z
),
scaleX:
1 +
(dummyThreeObject.scale.x / initialScale.x - 1) * scaleDamping,
scaleY:
1 +
(dummyThreeObject.scale.y / initialScale.y - 1) * scaleDamping,
scaleZ:
1 +
(dummyThreeObject.scale.z / initialScale.z - 1) * scaleDamping,
};
private _updateDummyLocation(
dummyThreeObject: THREE.Object3D,
lastEditableSelectedObject: gdjs.RuntimeObject,
threeTransformControls: THREE_ADDONS.TransformControls
) {
const threeObject = lastEditableSelectedObject.get3DRendererObject();
if (!threeObject) return;
dummyThreeObject.position.copy(threeObject.position);
dummyThreeObject.rotation.copy(threeObject.rotation);
dummyThreeObject.scale.copy(threeObject.scale);
if (threeTransformControls.mode === 'rotate') {
// This is only done for the rotate mode because it messes with the
// orientation of the scale mode.
dummyThreeObject.rotation.y = -dummyThreeObject.rotation.y;
dummyThreeObject.rotation.z = -dummyThreeObject.rotation.z;
this._hasSelectionActuallyMoved =
this._hasSelectionActuallyMoved ||
!dummyThreeObject.position.equals(initialPosition) ||
!dummyThreeObject.rotation.equals(initialRotation) ||
!dummyThreeObject.scale.equals(initialScale);
});
this._selectionControls = {
object: lastEditableSelectedObject,
dummyThreeObject,
threeTransformControls,
};
dummyThreeObject.position.set(
lastEditableSelectedObject.getCenterXInScene(),
lastEditableSelectedObject.getCenterYInScene(),
is3D(lastEditableSelectedObject)
? lastEditableSelectedObject.getCenterZInScene()
: 0
);
} else {
dummyThreeObject.position.set(
lastEditableSelectedObject.getX(),
lastEditableSelectedObject.getY(),
is3D(lastEditableSelectedObject)
? lastEditableSelectedObject.getZ()
: 0
);
}
}
@@ -2230,9 +2404,18 @@ namespace gdjs {
this._draggedNewObject.deleteFromScene();
this._draggedNewObject = null;
}
this._editorGrid.setVisible(false);
}
dragNewInstance({ name, dropped }: { name: string; dropped: boolean }) {
dragNewInstance({
name,
dropped,
isAltPressed,
}: {
name: string;
dropped: boolean;
isAltPressed: boolean;
}) {
const currentScene = this._currentScene;
if (!currentScene) return;
const editedInstanceContainer = this.getEditedInstanceContainer();
@@ -2241,6 +2424,8 @@ namespace gdjs {
const selectedLayer = this.getEditorLayer(this._selectedLayerName);
if (!selectedLayer) return;
const inputManager = this._runtimeGame.getInputManager();
if (this._draggedNewObject && this._draggedNewObject.getName() !== name) {
this._draggedNewObject.deleteFromScene();
this._draggedNewObject = null;
@@ -2269,19 +2454,48 @@ namespace gdjs {
// - stay still
// - drop the object
if (!dropped) {
let isCursorFound = false;
let cursorX = 0;
let cursorY = 0;
let cursorZ = 0;
if (is3D(this._draggedNewObject)) {
const cursor = this._getCursorIn3D([this._draggedNewObject]);
if (cursor) {
const [cursorX, cursorY, cursorZ] = cursor;
this._draggedNewObject.setX(cursorX);
this._draggedNewObject.setY(cursorY);
this._draggedNewObject.setZ(cursorZ);
[cursorX, cursorY, cursorZ] = cursor;
isCursorFound = true;
}
} else {
const projectedCursor = this._getProjectedCursor();
if (projectedCursor) {
this._draggedNewObject.setX(projectedCursor[0]);
this._draggedNewObject.setY(projectedCursor[1]);
[cursorX, cursorY] = projectedCursor;
isCursorFound = true;
}
}
if (isCursorFound) {
this._editorGrid.setNormal('Z');
this._editorGrid.setPosition(cursorX, cursorY, cursorZ);
const cameraLayer = this.getCameraLayer(
this._draggedNewObject.getLayer()
);
const threeScene = cameraLayer
? cameraLayer.getRenderer().getThreeScene()
: null;
if (threeScene) {
this._editorGrid.setTreeScene(threeScene);
}
this._editorGrid.setVisible(true);
if (this._editorGrid.isSpanningEnabled(inputManager, isAltPressed)) {
cursorX = this._editorGrid.getSnappedX(cursorX);
cursorY = this._editorGrid.getSnappedY(cursorY);
}
// TODO The object Z should be changed according to the new X and Y
// to match the ground.
this._draggedNewObject.setX(Math.round(cursorX));
this._draggedNewObject.setY(Math.round(cursorY));
// We don't round on Z because if cubes are stacked and there depth
// is not round it would leave an interstice between them.
if (is3D(this._draggedNewObject)) {
this._draggedNewObject.setZ(cursorZ);
}
}
}
@@ -2565,7 +2779,7 @@ namespace gdjs {
private _getCursorIn3D(
excludedObjects?: Array<gdjs.RuntimeObject>
): [float, float, float] | null {
): Point3D | null {
const closestIntersect =
this._getClosestIntersectionUnderCursor(excludedObjects);
if (closestIntersect) {
@@ -2657,6 +2871,9 @@ namespace gdjs {
) {
this._timeSinceLastInteraction = 0;
}
if (this._draggedNewObject) {
this._timeSinceLastInteraction = 0;
}
this._timeSinceLastInteraction += elapsedTime;
// Adapt the framerate to avoid consuming too much CPU when the editor is not visible
@@ -2694,13 +2911,31 @@ namespace gdjs {
this._pressedOriginalCursorY = inputManager.getCursorY();
}
if (!this._selectionControls) {
this._isTransformControlsHovered = false;
} else if (
this._previousCursorX !== inputManager.getMouseX() ||
this._previousCursorY !== inputManager.getMouseY()
) {
this._isTransformControlsHovered =
!!this._selectionControls.threeTransformControls.axis;
}
this._handleCameraMovement();
this._handleSelectedObjectDragging();
this._handleSelectionMovement();
this._updateSelectionBox();
this._handleSelection({ objectUnderCursor });
this._updateSelectionOutline({ objectUnderCursor });
this._updateSelectionControls();
// Custom objects only update their position at the end of the frame
// because they don't override position setters like built-in objects do.
// Since the instance position is not yet set when `onCreated` is called,
// they will be at (0; 0; 0) during the 1st step.
// When they are selected and `switchToSceneOrVariant` has just been
// called, it avoid to put the control at (0; 0; 0).
if (!this._isFirstFrame) {
this._updateSelectionControls();
}
this._updateInnerAreaOutline();
this._handleContextMenu();
this._handleShortcuts();
@@ -2712,11 +2947,14 @@ namespace gdjs {
}
this._wasMouseLeftButtonPressed = inputManager.isMouseButtonPressed(0);
this._wasMouseRightButtonPressed = inputManager.isMouseButtonPressed(1);
this._previousCursorX = inputManager.getMouseX();
this._previousCursorY = inputManager.getMouseY();
if (this._currentScene) {
this._currentScene._updateObjectsForInGameEditor();
this._currentScene.render();
}
this._isFirstFrame = false;
}
private _getEditorCamera(): EditorCamera {
@@ -2739,6 +2977,7 @@ namespace gdjs {
gridOffsetY: float = 0;
gridOffsetZ: float = 0;
gridColor: integer = 0;
gridAlpha: float = 1;
isSnappingEnabledByDefault = false;
threeScene: THREE.Scene | null = null;
@@ -2751,11 +2990,15 @@ namespace gdjs {
this.isForcefullyHidden = !instancesEditorSettings.grid;
this.gridWidth = instancesEditorSettings.gridWidth;
this.gridHeight = instancesEditorSettings.gridHeight;
this.gridDepth = instancesEditorSettings.gridDepth || 0;
this.gridDepth =
instancesEditorSettings.gridDepth === undefined
? 32
: instancesEditorSettings.gridDepth;
this.gridOffsetX = instancesEditorSettings.gridOffsetX;
this.gridOffsetY = instancesEditorSettings.gridOffsetY;
this.gridOffsetZ = instancesEditorSettings.gridOffsetZ || 0;
this.gridColor = instancesEditorSettings.gridColor;
this.gridAlpha = instancesEditorSettings.gridAlpha;
this.isSnappingEnabledByDefault = instancesEditorSettings.snap;
this.rebuildGrid();
}
@@ -2769,6 +3012,8 @@ namespace gdjs {
this.gridColor,
this.gridColor
);
this.gridHelper.material.transparent = true;
this.gridHelper.material.opacity = this.gridAlpha;
this.gridHelper.rotation.order = 'ZYX';
this.updateVisibility();
this.updateLocation();
@@ -2854,8 +3099,15 @@ namespace gdjs {
return snap(z, gridDepth || 0, gridOffsetY);
}
isSpanningEnabled(inputManager: gdjs.InputManager): boolean {
return this.isSnappingEnabledByDefault !== isAltPressed(inputManager);
isSpanningEnabled(
inputManager: gdjs.InputManager,
considerAltPressed?: boolean
): boolean {
const altPressed =
considerAltPressed === undefined
? isAltPressed(inputManager)
: considerAltPressed;
return this.isSnappingEnabledByDefault !== altPressed;
}
}
@@ -3364,7 +3616,7 @@ namespace gdjs {
return this.target.z;
}
private _getCameraForwardVector(): [float, float, float] {
private _getCameraForwardVector(): Point3D {
// Camera forward (from camera toward where it looks), unit length.
const cosYaw = Math.cos(gdjs.toRad(this.rotationAngle + 90));
const sinYaw = Math.sin(gdjs.toRad(this.rotationAngle + 90));

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

View File

@@ -631,7 +631,7 @@ namespace gdjs {
/**
* Create a new object from its name. The object is also added to the instances
* living in the container (No need to call {@link gdjs.RuntimeScene.addObject})
* living in the container (No need to call {@link addObject})
* @param objectName The name of the object to be created
* @return The created object
*/

View File

@@ -119,6 +119,101 @@ namespace gdjs {
}
}
getNetworkSyncData(): LayerNetworkSyncData {
const effectsNetworkSyncData = {};
for (const effectName in this._rendererEffects) {
effectsNetworkSyncData[effectName] =
this._rendererEffects[effectName].getNetworkSyncData();
}
return {
timeScale: this._timeScale,
defaultZOrder: this._defaultZOrder,
hidden: this._hidden,
effects: effectsNetworkSyncData,
followBaseLayerCamera: this._followBaseLayerCamera,
clearColor: this._clearColor,
cameraX: this.getCameraX(),
cameraY: this.getCameraY(),
cameraZ: this.getCameraZ(null),
cameraRotation: this.getCameraRotation(),
cameraZoom: this.getCameraZoom(),
};
}
updateFromNetworkSyncData(networkSyncData: LayerNetworkSyncData): void {
if (
networkSyncData.timeScale !== undefined &&
networkSyncData.timeScale !== this._timeScale
) {
this.setTimeScale(networkSyncData.timeScale);
}
if (
networkSyncData.defaultZOrder !== undefined &&
networkSyncData.defaultZOrder !== this._defaultZOrder
) {
this.setDefaultZOrder(networkSyncData.defaultZOrder);
}
if (
this._hidden != undefined &&
this._hidden !== networkSyncData.hidden
) {
this.show(!networkSyncData.hidden);
}
if (
networkSyncData.followBaseLayerCamera !== undefined &&
networkSyncData.followBaseLayerCamera !== this._followBaseLayerCamera
) {
this.setFollowBaseLayerCamera(networkSyncData.followBaseLayerCamera);
}
if (
networkSyncData.clearColor !== undefined &&
networkSyncData.clearColor !== this._clearColor
) {
this.setClearColor(
networkSyncData.clearColor[0] * 255,
networkSyncData.clearColor[1] * 255,
networkSyncData.clearColor[2] * 255
);
}
if (
networkSyncData.cameraX !== undefined &&
networkSyncData.cameraX !== this.getCameraX()
) {
this.setCameraX(networkSyncData.cameraX);
}
if (
networkSyncData.cameraY !== undefined &&
networkSyncData.cameraY !== this.getCameraY()
) {
this.setCameraY(networkSyncData.cameraY);
}
if (
networkSyncData.cameraZ !== undefined &&
networkSyncData.cameraZ !== this.getCameraZ(null)
) {
this.setCameraZ(networkSyncData.cameraZ, null);
}
if (
networkSyncData.cameraRotation !== undefined &&
networkSyncData.cameraRotation !== this.getCameraRotation()
) {
this.setCameraRotation(networkSyncData.cameraRotation);
}
if (
networkSyncData.cameraZoom !== undefined &&
networkSyncData.cameraZoom !== this.getCameraZoom()
) {
this.setCameraZoom(networkSyncData.cameraZoom);
}
for (const effectName in networkSyncData.effects) {
this._rendererEffects[effectName].updateFromNetworkSyncData(
networkSyncData.effects[effectName]
);
}
}
getRuntimeLayer(): gdjs.RuntimeLayer {
return this;
}

View File

@@ -271,17 +271,24 @@ namespace gdjs {
} else if (data.command === 'profiler.stop') {
runtimeGame.stopCurrentSceneProfiler();
} else if (data.command === 'hotReload') {
this._hasLoggedUncaughtException = false;
that._hotReloader
.hotReload({
projectData: data.payload.projectData,
runtimeGameOptions: data.payload.runtimeGameOptions,
shouldReloadResources:
data.payload.shouldReloadResources || false,
})
.then((logs) => {
that.sendHotReloaderLogs(logs);
});
const runtimeGameOptions: RuntimeGameOptions =
data.payload.runtimeGameOptions;
if (
(runtimeGameOptions.initialRuntimeGameStatus?.isInGameEdition ||
false) === runtimeGame.isInGameEdition()
) {
this._hasLoggedUncaughtException = false;
that._hotReloader
.hotReload({
projectData: data.payload.projectData,
runtimeGameOptions,
shouldReloadResources:
data.payload.shouldReloadResources || false,
})
.then((logs) => {
that.sendHotReloaderLogs(logs);
});
}
} else if (data.command === 'hotReloadObjects') {
if (inGameEditor) {
const editedInstanceContainer =
@@ -302,7 +309,7 @@ namespace gdjs {
data.payload.layers,
data.payload.areEffectsHidden
);
runtimeGame._data.areEffectsHiddenInEditor =
runtimeGame.getGameData().areEffectsHiddenInEditor =
data.payload.areEffectsHidden;
that._hotReloader.hotReloadRuntimeSceneLayers(
data.payload.layers,
@@ -407,6 +414,7 @@ namespace gdjs {
inGameEditor.dragNewInstance({
name: data.name,
dropped: data.dropped,
isAltPressed: data.isAltPressed,
});
} else if (data.command === 'cancelDragNewInstance') {
if (inGameEditor) inGameEditor.cancelDragNewInstance();

View File

@@ -386,7 +386,7 @@ namespace gdjs {
// It can't actually happen.
this._logs.push({
kind: 'error',
message: "Can't hot-reload as no scene are opened.",
message: "Can't hot-reload as no scene is opened.",
});
return;
}
@@ -815,7 +815,7 @@ namespace gdjs {
changedRuntimeBehaviors: ChangedRuntimeBehavior[],
runtimeInstanceContainer: gdjs.RuntimeInstanceContainer
): void {
if (!oldLayoutData.objects || newLayoutData.objects) {
if (!oldLayoutData.objects || !newLayoutData.objects) {
// It can happen when `hotReloadRuntimeInstances` is executed.
// `hotReloadRuntimeInstances` doesn't resolve the custom objects
// because it can only modify the 1st level of instances.

View File

@@ -410,7 +410,12 @@ namespace gdjs {
return true;
};
export const pickNearestObject = function (objectsLists, x, y, inverted) {
export const pickNearestObject = function (
objectsLists: ObjectsLists,
x: float,
y: float,
inverted?: boolean | undefined
) {
let bestObject = null;
let best = 0;
let first = true;
@@ -421,8 +426,7 @@ namespace gdjs {
for (let j = 0; j < list.length; ++j) {
const object = list[j];
const distance = object.getSqDistanceToPosition(x, y);
// @ts-ignore
if (first || (distance < best) ^ inverted) {
if (first || distance < best !== inverted) {
best = distance;
bestObject = object;
}
@@ -622,6 +626,14 @@ namespace gdjs {
}
}
const logger = new gdjs.Logger('LongLivedObjectsLists');
export type LongLivedObjectsListNetworkSyncData = {
objectsLists: {
[objectName: string]: Array<string>;
};
localVariablesContainers: Array<Array<VariableNetworkSyncData>>;
};
/**
* A container for objects lists that should last more than the current frame.
* It automatically removes objects that were destroyed from the objects lists.
@@ -695,5 +707,85 @@ namespace gdjs {
): void {
gdjs.copyArray(variablesContainers, this.localVariablesContainers);
}
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): LongLivedObjectsListNetworkSyncData {
const objectsLists: {
[objectName: string]: Array<string>;
} = {};
for (const [objectName, runtimeObjects] of this.objectsLists.entries()) {
const objectNetworkIds: Array<string> = [];
for (const runtimeObject of runtimeObjects) {
const objectNetworkId = runtimeObject.getNetworkId();
if (!objectNetworkId) {
logger.warn(
'Tried to get sync data of a LongLivedObjectsList and found an object without a network ID'
);
continue;
}
objectNetworkIds.push(objectNetworkId);
}
objectsLists[objectName] = objectNetworkIds;
}
return {
objectsLists,
localVariablesContainers: this.localVariablesContainers.map(
(container) => container.getNetworkSyncData(syncOptions)
),
};
}
updateFromNetworkSyncData(
syncData: LongLivedObjectsListNetworkSyncData,
runtimeScene: gdjs.RuntimeScene,
syncOptions: UpdateFromNetworkSyncDataOptions
) {
const { objectsLists, localVariablesContainers } = syncData;
// Clear the current state.
this.objectsLists.clear();
this.localVariablesContainers.length = 0;
// Restore the list of objects.
for (const [objectName, objectNetworkIds] of Object.entries(
objectsLists
)) {
const runtimeObjects = runtimeScene.getObjects(objectName);
if (!runtimeObjects) {
logger.warn(
'Tried to update sync data of a LongLivedObjectsList but cannot find objects with name: ' +
objectName
);
continue;
}
const runtimeObjectsFromSyncData = runtimeObjects.filter(
(runtimeObject) => {
const runtimeObjectNetworkId = runtimeObject.getNetworkId();
return (
!!runtimeObjectNetworkId &&
objectNetworkIds.includes(runtimeObjectNetworkId)
);
}
);
for (const runtimeObject of runtimeObjectsFromSyncData) {
this.addObject(objectName, runtimeObject);
}
}
// Restore the local variables containers.
this.localVariablesContainers = localVariablesContainers.map(
(localVariablesContainer) => {
const newContainer = new gdjs.VariablesContainer();
newContainer.updateFromNetworkSyncData(
localVariablesContainer,
syncOptions
);
return newContainer;
}
);
}
}
}

View File

@@ -143,6 +143,19 @@ namespace gdjs {
.getElapsedTime();
return this.timeElapsedOnScene >= this.duration;
}
getNetworkSyncData(): WaitTaskNetworkSyncData {
return {
type: 'wait',
duration: this.duration,
timeElapsedOnScene: this.timeElapsedOnScene,
};
}
updateFromNetworkSyncData(syncData: WaitTaskNetworkSyncData): void {
this.duration = syncData.duration;
this.timeElapsedOnScene = syncData.timeElapsedOnScene;
}
}
export const wait = (durationInSeconds: float): AsyncTask =>

View File

@@ -32,6 +32,16 @@ namespace gdjs {
instanceContainer: gdjs.RuntimeInstanceContainer,
runtimeObject: gdjs.RuntimeObject
) => void;
type RuntimeSceneGetSyncDataCallback = (
runtimeScene: gdjs.RuntimeScene,
currentSyncData: LayoutNetworkSyncData,
syncOptions: GetNetworkSyncDataOptions
) => void;
type RuntimeSceneUpdateFromSyncData = (
runtimeScene: gdjs.RuntimeScene,
receivedSyncData: LayoutNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) => void;
export const callbacksFirstRuntimeSceneLoaded: Array<RuntimeSceneCallback> =
[];
@@ -45,6 +55,10 @@ namespace gdjs {
export const callbacksRuntimeSceneUnloaded: Array<RuntimeSceneCallback> = [];
export const callbacksObjectDeletedFromScene: Array<RuntimeSceneRuntimeObjectCallback> =
[];
export const callbacksRuntimeSceneGetSyncData: Array<RuntimeSceneGetSyncDataCallback> =
[];
export const callbacksRuntimeSceneUpdateFromSyncData: Array<RuntimeSceneUpdateFromSyncData> =
[];
/** Base64 encoded logo of GDevelop for the splash screen. */
export let gdevelopLogo: string = '';
@@ -397,6 +411,28 @@ namespace gdjs {
gdjs.callbacksObjectDeletedFromScene.push(callback);
};
/**
* Register a function to be called each time a scene is getting its sync
* data retrieved (via getNetworkSyncData).
* @param callback The function to be called.
*/
export const registerRuntimeSceneGetSyncDataCallback = function (
callback: RuntimeSceneGetSyncDataCallback
): void {
gdjs.callbacksRuntimeSceneGetSyncData.push(callback);
};
/**
* Register a function to be called each time a scene is getting its sync
* data updated (via updateFromNetworkSyncData).
* @param callback The function to be called.
*/
export const registerRuntimeSceneUpdateFromSyncDataCallback = function (
callback: RuntimeSceneUpdateFromSyncData
): void {
gdjs.callbacksRuntimeSceneUpdateFromSyncData.push(callback);
};
/**
* Unregister a callback.
* This should not be used apart from the code generated from extensions

View File

@@ -30,6 +30,36 @@ namespace gdjs {
return volume;
};
/**
* Handles errors that occur when calling Howler sound methods.
*
* This function provides special handling for "Maximum call stack size exceeded" errors
* that can occur in Howler.js due to recursive method calls in volume(), seek(), fade(),
* and other sound manipulation methods. Instead of crashing the application, these
* specific errors are logged as warnings to allow the application to continue running.
*
* @param error - The error that occurred during the method call
* @param methodName - The name of the Howler method that caused the error (e.g., 'volume', 'seek')
*
* @throws {Error} Re-throws the original error if it's not a stack overflow error
*
*/
const handleHowlerSoundMethodError = (error: unknown, methodName: string) => {
if (
error instanceof Error &&
error.message &&
typeof error.message === 'string' &&
error.message.startsWith('Maximum call stack size exceeded')
) {
console.warn(
`An error occurred when call method "${methodName}":`,
error
);
} else {
throw error;
}
};
/**
* A thin wrapper around a Howl object with:
* * Handling of callbacks when the sound is not yet loaded.
@@ -84,11 +114,23 @@ namespace gdjs {
*/
private _onPlay: Array<HowlCallback> = [];
constructor(howl: Howl, volume: float, loop: boolean, rate: float) {
/**
* The filepath to the resource
*/
private _audioResourceName: string;
constructor(
howl: Howl,
volume: float,
loop: boolean,
rate: float,
audioResourceName: string
) {
this._howl = howl;
this._initialVolume = clampVolume(volume);
this._loop = loop;
this._rate = rate;
this._audioResourceName = audioResourceName;
}
/**
@@ -103,33 +145,36 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
play(): this {
if (this.isLoaded()) {
const newID = this._howl.play(
this._id === null ? '__default' : this._id
);
this._id = newID;
try {
if (this.isLoaded()) {
const newID = this._howl.play(
this._id === null ? '__default' : this._id
);
this._id = newID;
// Set the howl properties as soon as the sound is played and we have its ID.
this._howl.volume(this._initialVolume, newID); // this._initialVolume is already clamped between 0 and 1.
this._howl.loop(this._loop, newID);
// this._rate is not clamped, but we need to clamp it when passing it to Howler.js as it
// only supports a specific range.
this._howl.rate(gdjs.HowlerSoundManager.clampRate(this._rate), newID);
// Manually handle the play event before we have an ID.
// Before loading, howler won't register events as without an ID we cannot set a listener.
// Once we have an ID, we can transfer control of the events to howler.
// We also need to call them once as Howler doesn't for the first play event.
this._onPlay.forEach((func) => {
// Transfer the event to howler now that we have an ID
this.on('play', func);
func(newID);
});
this._oncePlay.forEach((func) => func(newID));
this._onPlay = [];
this._oncePlay = [];
} else this._howl.once('load', () => this.play()); // Play only once the howl is fully loaded
// Set the howl properties as soon as the sound is played and we have its ID.
this._howl.volume(this._initialVolume, newID); // this._initialVolume is already clamped between 0 and 1.
this._howl.loop(this._loop, newID);
// this._rate is not clamped, but we need to clamp it when passing it to Howler.js as it
// only supports a specific range.
this._howl.rate(gdjs.HowlerSoundManager.clampRate(this._rate), newID);
// Manually handle the play event before we have an ID.
// Before loading, howler won't register events as without an ID we cannot set a listener.
// Once we have an ID, we can transfer control of the events to howler.
// We also need to call them once as Howler doesn't for the first play event.
this._onPlay.forEach((func) => {
// Transfer the event to howler now that we have an ID
this.on('play', func);
func(newID);
});
this._oncePlay.forEach((func) => func(newID));
this._onPlay = [];
this._oncePlay = [];
} else this._howl.once('load', () => this.play()); // Play only once the howl is fully loaded
} catch (error) {
handleHowlerSoundMethodError(error, 'play');
}
return this;
}
@@ -138,7 +183,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
pause(): this {
if (this._id !== null) this._howl.pause(this._id);
try {
if (this._id !== null) this._howl.pause(this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'pause');
}
return this;
}
@@ -147,7 +196,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
stop(): this {
if (this._id !== null) this._howl.stop(this._id);
try {
if (this._id !== null) this._howl.stop(this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'stop');
}
return this;
}
@@ -158,9 +211,10 @@ namespace gdjs {
* to preload the sounds.
*/
playing(): boolean {
const isSoundPlaying =
this._id !== null ? this._howl.playing(this._id) : true;
return (
(this._id !== null ? this._howl.playing(this._id) : true) ||
!this.isLoaded() // Loading is considered playing
isSoundPlaying || !this.isLoaded() // Loading is considered playing
);
}
@@ -196,11 +250,15 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setRate(rate: float): this {
this._rate = rate;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) {
rate = gdjs.HowlerSoundManager.clampRate(rate);
this._howl.rate(rate, this._id);
try {
this._rate = rate;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) {
rate = gdjs.HowlerSoundManager.clampRate(rate);
this._howl.rate(rate, this._id);
}
} catch (error) {
handleHowlerSoundMethodError(error, 'rate');
}
return this;
}
@@ -217,9 +275,13 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setLoop(loop: boolean): this {
this._loop = loop;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.loop(loop, this._id);
try {
this._loop = loop;
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.loop(loop, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'loop');
}
return this;
}
@@ -239,10 +301,14 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setVolume(volume: float): this {
this._initialVolume = clampVolume(volume);
try {
this._initialVolume = clampVolume(volume);
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.volume(this._initialVolume, this._id);
// If the sound has already started playing, then change the value directly.
if (this._id !== null) this._howl.volume(this._initialVolume, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'volume');
}
return this;
}
@@ -259,7 +325,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setMute(mute: boolean): this {
if (this._id !== null) this._howl.mute(mute, this._id);
try {
if (this._id !== null) this._howl.mute(mute, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'mute');
}
return this;
}
@@ -276,7 +346,11 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
setSeek(seek: float): this {
if (this._id !== null) this._howl.seek(seek, this._id);
try {
if (this._id !== null) this._howl.seek(seek, this._id);
} catch (error) {
handleHowlerSoundMethodError(error, 'seek');
}
return this;
}
@@ -302,8 +376,17 @@ namespace gdjs {
* @returns The current instance for chaining.
*/
fade(from: float, to: float, duration: float): this {
if (this._id !== null)
this._howl.fade(clampVolume(from), clampVolume(to), duration, this._id);
try {
if (this._id !== null)
this._howl.fade(
clampVolume(from),
clampVolume(to),
duration,
this._id
);
} catch (error) {
handleHowlerSoundMethodError(error, 'fade');
}
return this;
}
@@ -357,10 +440,27 @@ namespace gdjs {
if (this._id !== null) this._howl.off(event, handler, this._id);
return this;
}
getNetworkSyncData(): SoundSyncData | undefined {
if (this.paused() || !this.isLoaded() || this.stopped()) return undefined;
// Seek can sometimes return the Howl object in case it isn't loaded yet, in this case we default to 0.
const seek = this.getSeek();
const numberSeek = typeof seek !== 'number' ? 0 : seek;
// If the Howl is still loading, we use the initialVolume, as the Howl
// has been initialized with volume 0.
const volume = this.isLoaded() ? this.getVolume() : this._initialVolume;
return {
resourceName: this._audioResourceName,
loop: this._loop,
volume,
rate: this._rate,
seek: numberSeek,
};
}
}
/**
* HowlerSoundManager is used to manage the sounds and musics of a RuntimeScene.
* HowlerSoundManager is used to manage the sounds and musics of a RuntimeGame.
*
* It is basically a container to associate channels to sounds and keep a list
* of all sounds being played.
@@ -609,8 +709,7 @@ namespace gdjs {
);
cacheContainer.set(resource, howl);
}
return new gdjs.HowlerSound(howl, volume, loop, rate);
return new gdjs.HowlerSound(howl, volume, loop, rate, soundName);
}
/**
@@ -708,7 +807,13 @@ namespace gdjs {
this._loadedSounds.clear();
}
playSound(soundName: string, loop: boolean, volume: float, pitch: float) {
playSound(
soundName: string,
loop: boolean,
volume: float,
pitch: float,
seek?: float
) {
const sound = this.createHowlerSound(
soundName,
/* isMusic= */ false,
@@ -724,6 +829,9 @@ namespace gdjs {
}
});
sound.play();
if (seek) {
sound.setSeek(seek);
}
}
playSoundOnChannel(
@@ -731,7 +839,8 @@ namespace gdjs {
channel: integer,
loop: boolean,
volume: float,
pitch: float
pitch: float,
seek?: float
) {
if (this._sounds[channel]) this._sounds[channel].stop();
@@ -756,13 +865,22 @@ namespace gdjs {
}
});
sound.play();
if (seek) {
sound.setSeek(seek);
}
}
getSoundOnChannel(channel: integer): HowlerSound | null {
return this._sounds[channel] || null;
}
playMusic(soundName: string, loop: boolean, volume: float, pitch: float) {
playMusic(
soundName: string,
loop: boolean,
volume: float,
pitch: float,
seek?: float
) {
const music = this.createHowlerSound(
soundName,
/* isMusic= */ true,
@@ -778,6 +896,9 @@ namespace gdjs {
}
});
music.play();
if (seek) {
music.setSeek(seek);
}
}
playMusicOnChannel(
@@ -785,7 +906,8 @@ namespace gdjs {
channel: integer,
loop: boolean,
volume: float,
pitch: float
pitch: float,
seek?: float
) {
if (this._musics[channel]) this._musics[channel].stop();
@@ -805,6 +927,9 @@ namespace gdjs {
}
});
music.play();
if (seek) {
music.setSeek(seek);
}
}
getMusicOnChannel(channel: integer): HowlerSound | null {
@@ -932,6 +1057,97 @@ namespace gdjs {
}
}
getNetworkSyncData(): SoundManagerSyncData {
const freeMusicsNetworkSyncData: SoundSyncData[] = [];
this._freeMusics.forEach((freeMusic) => {
const musicSyncData = freeMusic.getNetworkSyncData();
if (musicSyncData) freeMusicsNetworkSyncData.push(musicSyncData);
});
const freeSoundsNetworkSyncData: SoundSyncData[] = [];
this._freeSounds.forEach((freeSound) => {
const soundSyncData = freeSound.getNetworkSyncData();
if (soundSyncData) freeSoundsNetworkSyncData.push(soundSyncData);
});
const musicsNetworkSyncData: ChannelsSoundSyncData = {};
Object.entries(this._musics).forEach(([channel, music]) => {
const musicSyncData = music.getNetworkSyncData();
if (musicSyncData) {
const channelNumber = parseInt(channel, 10);
musicsNetworkSyncData[channelNumber] = musicSyncData;
}
});
const soundsNetworkSyncData: ChannelsSoundSyncData = {};
Object.entries(this._sounds).forEach(([channel, sound]) => {
const soundSyncData = sound.getNetworkSyncData();
if (soundSyncData) {
const channelNumber = parseInt(channel, 10);
soundsNetworkSyncData[channelNumber] = soundSyncData;
}
});
return {
globalVolume: this._globalVolume,
cachedSpatialPosition: this._cachedSpatialPosition,
freeMusics: freeMusicsNetworkSyncData,
freeSounds: freeSoundsNetworkSyncData,
musics: musicsNetworkSyncData,
sounds: soundsNetworkSyncData,
};
}
updateFromNetworkSyncData(syncData: SoundManagerSyncData): void {
this.clearAll();
this._globalVolume = syncData.globalVolume;
this._cachedSpatialPosition = syncData.cachedSpatialPosition;
for (let i = 0; i < syncData.freeSounds.length; i++) {
const freeSoundsSyncData: SoundSyncData = syncData.freeSounds[i];
this.playSound(
freeSoundsSyncData.resourceName,
freeSoundsSyncData.loop,
freeSoundsSyncData.volume * 100,
freeSoundsSyncData.rate,
freeSoundsSyncData.seek
);
}
for (let i = 0; i < syncData.freeMusics.length; i++) {
const freeMusicsSyncData: SoundSyncData = syncData.freeMusics[i];
this.playMusic(
freeMusicsSyncData.resourceName,
freeMusicsSyncData.loop,
freeMusicsSyncData.volume * 100,
freeMusicsSyncData.rate,
freeMusicsSyncData.seek
);
}
for (const [channel, soundSyncData] of Object.entries(syncData.sounds)) {
const channelNumber = parseInt(channel, 10);
this.playSoundOnChannel(
soundSyncData.resourceName,
channelNumber,
soundSyncData.loop,
soundSyncData.volume * 100,
soundSyncData.rate,
soundSyncData.seek
);
}
for (const [channel, musicSyncData] of Object.entries(syncData.musics)) {
const channelNumber = parseInt(channel, 10);
this.playMusicOnChannel(
musicSyncData.resourceName,
channelNumber,
musicSyncData.loop,
musicSyncData.volume * 100,
musicSyncData.rate,
musicSyncData.seek
);
}
}
/**
* To be called when the game is disposed.
* Unloads all audio from memory, clear Howl cache and stop all audio.

105
GDJS/Runtime/indexeddb.ts Normal file
View File

@@ -0,0 +1,105 @@
/*
* GDevelop JS Platform
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
export namespace indexedDb {
export const loadFromIndexedDB = async function (
dbName: string,
objectStoreName: string,
key: string
): Promise<any> {
return new Promise((resolve, reject) => {
try {
const request = indexedDB.open(dbName, 1);
request.onupgradeneeded = function () {
const db = request.result;
if (!db.objectStoreNames.contains(objectStoreName)) {
db.createObjectStore(objectStoreName);
}
};
request.onsuccess = function () {
const db = request.result;
const tx = db.transaction(objectStoreName, 'readonly');
const store = tx.objectStore(objectStoreName);
const getRequest = store.get(key);
getRequest.onsuccess = function () {
if (getRequest.result !== undefined) {
resolve(getRequest.result);
} else {
resolve(null);
}
};
getRequest.onerror = function () {
console.error(
'Error loading data from IndexedDB:',
getRequest.error
);
reject(getRequest.error);
};
};
request.onerror = function () {
console.error('Error opening IndexedDB:', request.error);
reject(request.error);
};
} catch (err) {
console.error('Exception thrown while opening IndexedDB:', err);
reject(err);
return;
}
});
};
export const saveToIndexedDB = async function (
dbName: string,
objectStoreName: string,
key: string,
data: any
): Promise<void> {
return new Promise((resolve, reject) => {
try {
const request = indexedDB.open(dbName, 1);
request.onupgradeneeded = function (event) {
const db = request.result;
if (!db.objectStoreNames.contains(objectStoreName)) {
db.createObjectStore(objectStoreName);
}
};
request.onsuccess = function () {
const db = request.result;
const tx = db.transaction(objectStoreName, 'readwrite');
const store = tx.objectStore(objectStoreName);
const putRequest = store.put(data, key);
putRequest.onsuccess = function () {
resolve();
};
putRequest.onerror = function () {
console.error(
'Error saving data to IndexedDB:',
putRequest.error
);
reject(putRequest.error);
};
};
request.onerror = function () {
console.error('Error opening IndexedDB:', request.error);
reject(request.error);
};
} catch (err) {
console.error('Exception thrown while opening IndexedDB:', err);
reject(err);
return;
}
});
};
}
}

View File

@@ -8,6 +8,11 @@ namespace gdjs {
* OnceTriggers is used to store the status of the conditions "Trigger once",
* that are used in events to have conditions that are only valid for one frame in a row.
*/
type OnceTriggersSyncData = {
onceTriggers: Record<integer, boolean>;
lastFrameOnceTriggers: Record<integer, boolean>;
};
export class OnceTriggers {
_onceTriggers: Record<integer, boolean> = {};
_lastFrameOnceTrigger: Record<integer, boolean> = {};
@@ -40,5 +45,17 @@ namespace gdjs {
this._onceTriggers[triggerId] = true;
return !this._lastFrameOnceTrigger.hasOwnProperty(triggerId);
}
getNetworkSyncData(): OnceTriggersSyncData {
return {
onceTriggers: this._onceTriggers,
lastFrameOnceTriggers: this._lastFrameOnceTrigger,
};
}
updateNetworkSyncData(data: OnceTriggersSyncData): void {
this._onceTriggers = data.onceTriggers;
this._lastFrameOnceTrigger = data.lastFrameOnceTriggers;
}
}
}

View File

@@ -77,7 +77,9 @@ namespace gdjs {
return false;
}
getNetworkSyncData(): BehaviorNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): BehaviorNetworkSyncData {
// To be redefined by behaviors that need to synchronize properties
// while calling super() to get the common properties.
return {
@@ -90,7 +92,10 @@ namespace gdjs {
* Update the behavior properties using the provided data.
* @param networkSyncData The new properties of the behavior.
*/
updateFromNetworkSyncData(networkSyncData: BehaviorNetworkSyncData): void {
updateFromNetworkSyncData(
networkSyncData: BehaviorNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
): void {
// Must be redefined by behaviors that need to synchronize properties
// while calling super() to get the common properties.
if (networkSyncData.act !== this._activated) {

View File

@@ -1631,6 +1631,9 @@ namespace gdjs {
): GameNetworkSyncData | null {
const syncData: GameNetworkSyncData = {
var: this._variables.getNetworkSyncData(syncOptions),
sm: syncOptions.syncSounds
? this.getSoundManager().getNetworkSyncData()
: undefined,
ss: this._sceneStack.getNetworkSyncData(syncOptions) || undefined,
};
@@ -1658,10 +1661,16 @@ namespace gdjs {
return syncData;
}
updateFromNetworkSyncData(syncData: GameNetworkSyncData) {
updateFromNetworkSyncData(
syncData: GameNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
this._throwIfDisposed();
if (syncData.var) {
this._variables.updateFromNetworkSyncData(syncData.var);
this._variables.updateFromNetworkSyncData(syncData.var, options);
}
if (syncData.sm) {
this.getSoundManager().updateFromNetworkSyncData(syncData.sm);
}
if (syncData.ss) {
this._sceneStack.updateFromNetworkSyncData(syncData.ss);
@@ -1676,7 +1685,8 @@ namespace gdjs {
this.getVariablesForExtension(extensionName);
if (extensionVariables) {
extensionVariables.updateFromNetworkSyncData(
extensionVariablesData
extensionVariablesData,
options
);
}
}

View File

@@ -422,7 +422,8 @@ namespace gdjs {
updatePreRender(instanceContainer: gdjs.RuntimeInstanceContainer): void {}
/**
* Called when the object is created from an initial instance at the startup of the scene.<br>
* Called when the object is created from an initial instance at the startup of the scene.
*
* Note that common properties (position, angle, z order...) have already been setup.
*
* @param initialInstanceData The data of the initial instance.
@@ -452,17 +453,17 @@ namespace gdjs {
* This can be redefined by objects to send more information.
* @returns The full network sync data.
*/
getNetworkSyncData(): ObjectNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): ObjectNetworkSyncData {
const behaviorNetworkSyncData = {};
this._behaviors.forEach((behavior) => {
if (!behavior.isSyncedOverNetwork()) {
if (!behavior.isSyncedOverNetwork() && !syncOptions.syncAllBehaviors) {
return;
}
const networkSyncData = behavior.getNetworkSyncData();
if (networkSyncData) {
behaviorNetworkSyncData[behavior.getName()] = networkSyncData;
}
const networkSyncData = behavior.getNetworkSyncData(syncOptions);
behaviorNetworkSyncData[behavior.getName()] = networkSyncData;
});
const variablesNetworkSyncData = this._variables.getNetworkSyncData({
@@ -481,7 +482,7 @@ namespace gdjs {
this._timers.items[timerName].getNetworkSyncData();
}
return {
const networkSyncData: ObjectNetworkSyncData = {
x: this.x,
y: this.y,
w: this.getWidth(),
@@ -498,6 +499,19 @@ namespace gdjs {
eff: effectsNetworkSyncData,
tim: timersNetworkSyncData,
};
if (syncOptions.syncObjectIdentifiers) {
networkSyncData.n = this.name;
if (!this.networkId) {
// If this is the first time the object is synced
// with identifier, then generate a networkId,
// so it can be re-used for future syncs.
this.networkId = gdjs.makeUuid().substring(0, 8);
}
networkSyncData.networkId = this.networkId;
}
return networkSyncData;
}
/**
@@ -507,7 +521,10 @@ namespace gdjs {
* @param networkSyncData The new data for the object.
* @returns true if the object was updated, false if it could not (i.e: network sync is not supported).
*/
updateFromNetworkSyncData(networkSyncData: ObjectNetworkSyncData) {
updateFromNetworkSyncData(
networkSyncData: ObjectNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
if (networkSyncData.x !== undefined) {
this.setX(networkSyncData.x);
}
@@ -562,20 +579,21 @@ namespace gdjs {
this._permanentForceY = networkSyncData.pfy;
}
// If variables are synchronized, update them first,
// as behaviors may depend on them. (Like tweens)
if (networkSyncData.var) {
this._variables.updateFromNetworkSyncData(networkSyncData.var, options);
}
// Loop through all behaviors and update them.
for (const behaviorName in networkSyncData.beh) {
const behaviorNetworkSyncData = networkSyncData.beh[behaviorName];
const behavior = this.getBehavior(behaviorName);
if (behavior) {
behavior.updateFromNetworkSyncData(behaviorNetworkSyncData);
behavior.updateFromNetworkSyncData(behaviorNetworkSyncData, options);
}
}
// If variables are synchronized, update them.
if (networkSyncData.var) {
this._variables.updateFromNetworkSyncData(networkSyncData.var);
}
// If effects are synchronized, update them.
if (networkSyncData.eff) {
// Loop through all effects and update them.
@@ -589,16 +607,19 @@ namespace gdjs {
}
// If timers are synchronized, update them.
// TODO: If a timer is removed, also remove it from the object?
if (networkSyncData.tim) {
this._timers.clear();
for (const timerName in networkSyncData.tim) {
const timerNetworkSyncData = networkSyncData.tim[timerName];
const timer = this._timers.get(timerName);
if (timer) {
timer.updateFromNetworkSyncData(timerNetworkSyncData);
}
const timerData = networkSyncData.tim[timerName];
const newTimer = new gdjs.Timer(timerData.name);
newTimer.updateFromNetworkSyncData(timerData);
this._timers.put(timerName, newTimer);
}
}
if (networkSyncData.networkId !== undefined) {
this.networkId = networkSyncData.networkId;
}
}
/**
@@ -677,7 +698,7 @@ namespace gdjs {
}
/**
* @return The internal object for a 3D rendering (PIXI.DisplayObject...)
* @return The internal object for a 3D rendering (THREE.Object3D...)
*/
get3DRendererObject(): THREE.Object3D | null | undefined {
return undefined;
@@ -701,8 +722,10 @@ namespace gdjs {
}
/**
* Get the unique identifier of the object.<br>
* The identifier is set by the runtimeScene owning the object.<br>
* Get the unique identifier of the object.
*
* The identifier is set by the runtimeScene owning the object.
*
* You can also use the id property (this._object.id) for increased efficiency instead of
* calling this method.
*
@@ -712,6 +735,18 @@ namespace gdjs {
return this.id;
}
/**
* Get the network ID of the object.
*
* The network ID is used to identify the object in a networked game.
* Or, for Save/Load purposes.
*
* @return The network ID of the object.
*/
getNetworkId(): string | null {
return this.networkId;
}
/**
* Set the position of the object.
*
@@ -807,6 +842,12 @@ namespace gdjs {
return this.getY();
}
/**
* Rotate the object towards another object position.
* @param x The target x position
* @param y The target y position
* @param speed The rotation speed. 0 for an immediate rotation to the target position.
*/
rotateTowardPosition(x: float, y: float, speed: float): void {
this.rotateTowardAngle(
gdjs.toDegrees(
@@ -820,8 +861,24 @@ namespace gdjs {
}
/**
* @param angle The targeted direction angle.
* @param speed The rotation speed.
* Rotate the object towards another object position (aiming at the center of the object).
* @param target The target object
* @param speed The rotation speed. 0 for an immediate rotation to the target object.
*/
rotateTowardObject(target: gdjs.RuntimeObject | null, speed: float): void {
if (target === null) {
return;
}
this.rotateTowardPosition(
target.getDrawableX() + target.getCenterX(),
target.getDrawableY() + target.getCenterY(),
speed
);
}
/**
* @param angle The targeted angle.
* @param speed The rotation speed. 0 for an immediate rotation to the target angle.
*/
rotateTowardAngle(angle: float, speed: float): void {
if (speed === 0) {
@@ -1488,7 +1545,8 @@ namespace gdjs {
//Forces :
/**
* Get a force from the garbage, or create a new force is garbage is empty.<br>
* Get a force from the garbage, or create a new force is garbage is empty.
*
* To be used each time a force is created so as to avoid temporaries objects.
*
* @param x The x coordinates of the force
@@ -1573,7 +1631,8 @@ namespace gdjs {
}
/**
* Add a force oriented toward another object.<br>
* Add a force oriented toward another object.
*
* (Shortcut for addForceTowardPosition)
* @param object The target object
* @param len The force length, in pixels.

View File

@@ -12,7 +12,13 @@ namespace gdjs {
*/
export class RuntimeScene extends gdjs.RuntimeInstanceContainer {
_eventsFunction: null | ((runtimeScene: RuntimeScene) => void) = null;
_idToCallbackMap: null | Map<
string,
(
runtimeScene: gdjs.RuntimeScene,
asyncObjectsList: gdjs.LongLivedObjectsList
) => void
> = null;
_renderer: RuntimeSceneRenderer;
_debuggerRenderer: gdjs.DebuggerRenderer;
_variables: gdjs.VariablesContainer;
@@ -128,7 +134,10 @@ namespace gdjs {
*/
loadFromScene(
sceneAndExtensionsData: SceneAndExtensionsData | null,
options?: { skipCreatingInstances?: boolean }
options?: {
skipCreatingInstances?: boolean;
skipStoppingSoundsOnStartup?: boolean;
}
) {
if (!sceneAndExtensionsData) {
logger.error('loadFromScene was called without a scene');
@@ -215,7 +224,11 @@ namespace gdjs {
for (let i = 0; i < gdjs.callbacksRuntimeSceneLoaded.length; ++i) {
gdjs.callbacksRuntimeSceneLoaded[i](this);
}
if (sceneData.stopSoundsOnStartup && this._runtimeGame) {
if (
sceneData.stopSoundsOnStartup &&
(!options || !options.skipStoppingSoundsOnStartup) &&
this._runtimeGame
) {
this._runtimeGame.getSoundManager().clearAll();
}
this._isLoaded = true;
@@ -342,6 +355,8 @@ namespace gdjs {
const module = gdjs[sceneData.mangledName + 'Code'];
if (module && module.func) {
this._eventsFunction = module.func;
this._idToCallbackMap =
gdjs[sceneData.mangledName + 'Code'].idToCallbackMap;
} else {
setupWarningLogger.warn(
'No function found for running logic of scene ' + this._name
@@ -836,27 +851,69 @@ namespace gdjs {
if (
syncedPlayerNumber !== undefined &&
syncedPlayerNumber !== 1 &&
(!this.networkId ||
(variablesNetworkSyncData.length === 0 &&
!Object.keys(extensionsVariablesSyncData).length))
!this.networkId
) {
// If we are getting sync data for a specific player,
// and they are not the host, there is no sync data to send if:
// - The scene has no networkId (it's either not a multiplayer scene or the scene is not yet networked).
// - There are no variables to sync in the scene or extensions.
// and they are not the host, there is no sync data to send if
// the scene has no networkId (it's either not a multiplayer scene or the scene is not yet networked).
return null;
}
return {
const networkSyncData: LayoutNetworkSyncData = {
var: variablesNetworkSyncData,
extVar: extensionsVariablesSyncData,
id: this.getOrCreateNetworkId(),
};
if (syncOptions.syncSceneVisualProps) {
networkSyncData.color = this._backgroundColor;
}
if (syncOptions.syncLayers) {
const layersSyncData = {};
for (const layerName in this._layers.items) {
layersSyncData[layerName] =
this._layers.items[layerName].getNetworkSyncData();
}
networkSyncData.layers = layersSyncData;
}
if (syncOptions.syncSceneTimers) {
networkSyncData.time = this._timeManager.getNetworkSyncData();
}
if (syncOptions.syncOnceTriggers) {
networkSyncData.once = this._onceTriggers.getNetworkSyncData();
}
gdjs.callbacksRuntimeSceneGetSyncData.forEach((callback) => {
callback(this, networkSyncData, syncOptions);
});
if (syncOptions.syncAsyncTasks) {
networkSyncData.async =
this._asyncTasksManager.getNetworkSyncData(syncOptions);
}
return networkSyncData;
}
updateFromNetworkSyncData(syncData: LayoutNetworkSyncData) {
updateFromNetworkSyncData(
syncData: LayoutNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
if (syncData.color !== undefined) {
this._backgroundColor = syncData.color;
}
if (syncData.layers) {
for (const layerName in syncData.layers) {
const layerData = syncData.layers[layerName];
if (this.hasLayer(layerName)) {
const layer = this.getLayer(layerName);
layer.updateFromNetworkSyncData(layerData);
}
}
}
// Update variables before anything else, as they might be used
// in other sync data (for instance in tweens).
if (syncData.var) {
this._variables.updateFromNetworkSyncData(syncData.var);
this._variables.updateFromNetworkSyncData(syncData.var, options);
}
if (syncData.extVar) {
for (const extensionName in syncData.extVar) {
@@ -868,11 +925,32 @@ namespace gdjs {
this._variablesByExtensionName.get(extensionName);
if (extensionVariables) {
extensionVariables.updateFromNetworkSyncData(
extensionVariablesData
extensionVariablesData,
options
);
}
}
}
if (syncData.time) {
this._timeManager.updateFromNetworkSyncData(syncData.time);
}
if (syncData.once) {
this._onceTriggers.updateNetworkSyncData(syncData.once);
}
gdjs.callbacksRuntimeSceneUpdateFromSyncData.forEach((callback) => {
callback(this, syncData, options);
});
// Sync Async last, as it might depend on other data.
if (syncData.async && this._idToCallbackMap) {
this._asyncTasksManager.updateFromNetworkSyncData(
syncData.async,
this._idToCallbackMap,
this,
options
);
}
}
getOrCreateNetworkId(): string {

View File

@@ -6,6 +6,7 @@ namespace gdjs {
sceneName: string;
externalLayoutName?: string;
skipCreatingInstancesFromScene?: boolean;
skipStoppingSoundsOnStartup?: boolean;
}
interface ReplaceSceneOptions extends PushSceneOptions {
@@ -142,7 +143,6 @@ namespace gdjs {
deprecatedExternalLayoutName?: string
): gdjs.RuntimeScene | null {
this._throwIfDisposed();
console.log({ options, deprecatedExternalLayoutName });
const sceneName =
typeof options === 'string' ? options : options.sceneName;
@@ -150,6 +150,10 @@ namespace gdjs {
typeof options === 'string'
? false
: options.skipCreatingInstancesFromScene;
const skipStoppingSoundsOnStartup =
typeof options === 'string'
? false
: options.skipStoppingSoundsOnStartup;
const externalLayoutName =
deprecatedExternalLayoutName ||
(typeof options === 'string' ? undefined : options.externalLayoutName);
@@ -167,6 +171,7 @@ namespace gdjs {
sceneName,
externalLayoutName,
skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup,
});
}
@@ -176,6 +181,7 @@ namespace gdjs {
sceneName,
externalLayoutName,
skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup,
});
this._isNextLayoutLoading = false;
});
@@ -192,6 +198,7 @@ namespace gdjs {
this._runtimeGame.getSceneAndExtensionsData(options.sceneName),
{
skipCreatingInstances: options.skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup: options.skipStoppingSoundsOnStartup,
}
);
this._wasFirstSceneLoaded = true;
@@ -271,6 +278,11 @@ namespace gdjs {
return this._wasFirstSceneLoaded;
}
getAllScenes(): Array<gdjs.RuntimeScene> {
this._throwIfDisposed();
return this._stack;
}
getAllSceneNames(): Array<string> {
this._throwIfDisposed();
return this._stack.map((scene) => scene.getName());
@@ -311,7 +323,9 @@ namespace gdjs {
this._sceneStackSyncDataToApply = sceneStackSyncData;
}
applyUpdateFromNetworkSyncDataIfAny(): boolean {
applyUpdateFromNetworkSyncDataIfAny(
options?: UpdateFromNetworkSyncDataOptions
): boolean {
this._throwIfDisposed();
const sceneStackSyncData = this._sceneStackSyncDataToApply;
let hasMadeChangeToStack = false;
@@ -319,6 +333,32 @@ namespace gdjs {
this._sceneStackSyncDataToApply = null;
const skipCreatingInstancesFromScene =
!!options && !!options.preventInitialInstancesCreation;
const skipStoppingSoundsOnStartup =
!!options && !!options.preventSoundsStoppingOnStartup;
if (options && options.clearSceneStack) {
while (this._stack.length !== 0) {
let scene = this._stack.pop();
if (scene) {
scene.unloadScene();
}
}
for (let i = 0; i < sceneStackSyncData.length; ++i) {
const sceneSyncData = sceneStackSyncData[i];
const newScene = this.push({
sceneName: sceneSyncData.name,
skipCreatingInstancesFromScene,
skipStoppingSoundsOnStartup,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
}
hasMadeChangeToStack = true;
return hasMadeChangeToStack;
}
// If this method is called, we are a client.
// We trust the host to be the source of truth for the scene stack.
// So we loop through the scenes in the stack given by the host and either:
@@ -328,12 +368,16 @@ namespace gdjs {
for (let i = 0; i < sceneStackSyncData.length; ++i) {
const sceneSyncData = sceneStackSyncData[i];
const sceneAtThisPositionInOurStack = this._stack[i];
if (!sceneAtThisPositionInOurStack) {
debugLogger.info(
`Scene at position ${i} with name ${sceneSyncData.name} is missing from the stack, adding it.`
);
// We have fewer scenes in the stack than the host, let's add the scene.
const newScene = this.push(sceneSyncData.name);
const newScene = this.push({
sceneName: sceneSyncData.name,
skipCreatingInstancesFromScene,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
@@ -350,10 +394,12 @@ namespace gdjs {
);
// The scene does not correspond to the scene at this position in our stack
// Let's unload everything after this position to recreate the stack.
const newScene = this.replace(
sceneSyncData.name,
true // Clear the stack
);
const newScene = this.replace({
sceneName: sceneSyncData.name,
clear: true,
skipCreatingInstancesFromScene,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}
@@ -393,10 +439,11 @@ namespace gdjs {
// This can happen if the host has restarted the scene
// We can't just update the networkId of the scene in the stack
// We need to replace it with a new scene
const newScene = this.replace(
sceneSyncData.name,
false // Don't clear the stack
);
const newScene = this.replace({
sceneName: sceneSyncData.name,
clear: false,
skipCreatingInstancesFromScene,
});
if (newScene) {
newScene.networkId = sceneSyncData.networkId;
}

View File

@@ -115,9 +115,11 @@ namespace gdjs {
return true;
}
getNetworkSyncData(): SpriteNetworkSyncData {
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): SpriteNetworkSyncData {
return {
...super.getNetworkSyncData(),
...super.getNetworkSyncData(syncOptions),
anim: this._animator.getNetworkSyncData(),
ifx: this.isFlippedX(),
ify: this.isFlippedY(),
@@ -128,8 +130,11 @@ namespace gdjs {
};
}
updateFromNetworkSyncData(newNetworkSyncData: SpriteNetworkSyncData) {
super.updateFromNetworkSyncData(newNetworkSyncData);
updateFromNetworkSyncData(
newNetworkSyncData: SpriteNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
super.updateFromNetworkSyncData(newNetworkSyncData, options);
if (newNetworkSyncData.ifx !== undefined) {
this.flipX(newNetworkSyncData.ifx);
}
@@ -954,14 +959,7 @@ namespace gdjs {
* @deprecated
*/
turnTowardObject(obj: gdjs.RuntimeObject | null) {
if (obj === null) {
return;
}
this.rotateTowardPosition(
obj.getDrawableX() + obj.getCenterX(),
obj.getDrawableY() + obj.getCenterY(),
0
);
return this.rotateTowardObject(obj, 0);
}
}
gdjs.registerObject(

View File

@@ -9,6 +9,15 @@ namespace gdjs {
* frame, since the beginning of the scene and other time related values.
* All durations are expressed in milliseconds.
*/
declare interface TimeManagerSyncData {
elapsedTime: float;
timeScale: float;
timeFromStart: float;
firstFrame: boolean;
timers: Hashtable<TimerNetworkSyncData>;
firstUpdateDone: boolean;
}
export class TimeManager {
_elapsedTime: float = 0;
_timeScale: float = 1;
@@ -59,6 +68,48 @@ namespace gdjs {
}
}
getNetworkSyncData(): TimeManagerSyncData {
const timerNetworkSyncDatas = new Hashtable<TimerNetworkSyncData>();
Object.entries(this._timers.items).forEach(([key, timer]) => {
timerNetworkSyncDatas.put(key, timer.getNetworkSyncData());
});
return {
elapsedTime: this._elapsedTime,
timeScale: this._timeScale,
timeFromStart: this._timeFromStart,
firstFrame: this._firstFrame,
timers: timerNetworkSyncDatas,
firstUpdateDone: this._firstUpdateDone,
};
}
updateFromNetworkSyncData(syncData: TimeManagerSyncData): void {
if (syncData.elapsedTime !== undefined) {
this._elapsedTime = syncData.elapsedTime;
}
if (syncData.timeScale !== undefined) {
this._timeScale = syncData.timeScale;
}
if (syncData.timeFromStart !== undefined) {
this._timeFromStart = syncData.timeFromStart;
}
if (syncData.firstFrame !== undefined) {
this._firstFrame = syncData.firstFrame;
}
if (syncData.timers !== undefined) {
this._timers.clear();
Object.entries(syncData.timers.items).forEach(([key, timerData]) => {
const newTimer = new gdjs.Timer(timerData.name);
newTimer.updateFromNetworkSyncData(timerData);
this._timers.put(key, newTimer);
});
}
if (syncData.firstUpdateDone !== undefined) {
this._firstUpdateDone = syncData.firstUpdateDone;
}
}
/**
* Get the time scale.
* @return The time scale (positive, 1 is normal speed).

View File

@@ -77,6 +77,7 @@ namespace gdjs {
getNetworkSyncData(): TimerNetworkSyncData {
return {
name: this._name,
time: this._time,
paused: this._paused,
};

View File

@@ -43,6 +43,26 @@ declare type ObjectData = {
declare type GetNetworkSyncDataOptions = {
playerNumber?: number;
isHost?: boolean;
syncObjectIdentifiers?: boolean;
syncAllVariables?: boolean;
syncAllBehaviors?: boolean;
syncSceneTimers?: boolean;
syncOnceTriggers?: boolean;
syncSounds?: boolean;
syncTweens?: boolean;
syncLayers?: boolean;
syncAsyncTasks?: boolean;
syncSceneVisualProps?: boolean;
syncFullTileMaps?: boolean;
};
declare type UpdateFromNetworkSyncDataOptions = {
clearSceneStack?: boolean;
preventInitialInstancesCreation?: boolean;
preventSoundsStoppingOnStartup?: boolean;
clearInputs?: boolean;
keepControl?: boolean;
ignoreVariableOwnership?: boolean;
};
/** Object containing basic properties for all objects synchronizing over the network. */
@@ -71,6 +91,10 @@ declare type BasicObjectNetworkSyncData = {
pfx: number;
/** Permanent force on Y */
pfy: number;
/** Name of the object */
n?: string;
/** The network ID of the instance. */
networkId?: string;
};
/**
@@ -92,6 +116,8 @@ declare interface ObjectNetworkSyncData extends BasicObjectNetworkSyncData {
tim?: {
[timerName: string]: TimerNetworkSyncData;
};
/** Tweens */
tween?: TweenManagerNetworkSyncData;
}
declare type ForceNetworkSyncData = {
@@ -103,6 +129,7 @@ declare type ForceNetworkSyncData = {
};
declare type TimerNetworkSyncData = {
name: string;
time: float;
paused: boolean;
};
@@ -129,12 +156,30 @@ declare type VariableData = Readonly<{
/** A variable child of a container. Those always have a name. */
declare type RootVariableData = Omit<VariableData, 'name'> & { name: string };
declare type VariableNetworkSyncData = {
name: string;
declare type UnnamedVariableNetworkSyncData = {
value: string | float | boolean;
children?: VariableNetworkSyncData[];
type: VariableType;
owner: number;
owner: number | null;
};
declare type VariableNetworkSyncData = UnnamedVariableNetworkSyncData & {
name: string;
};
declare type LayerNetworkSyncData = {
timeScale: float;
defaultZOrder: integer;
hidden: boolean;
effects: {
[effectName: string]: EffectNetworkSyncData;
};
followBaseLayerCamera: boolean;
clearColor: Array<integer>;
cameraX: float;
cameraY: float;
cameraZ: float;
cameraRotation: float;
cameraZoom: float;
};
/** Properties to set up a behavior. */
@@ -150,6 +195,74 @@ declare type BehaviorNetworkSyncData = {
props: any;
};
declare type SceneTweenType =
| 'layoutValue'
| 'layerValue'
| 'variable'
| 'cameraZoom'
| 'cameraRotation'
| 'cameraPosition'
| 'colorEffectProperty'
| 'numberEffectProperty';
declare type ObjectTweenType =
| 'variable'
| 'position'
| 'positionX'
| 'positionY'
| 'positionZ'
| 'width'
| 'height'
| 'depth'
| 'angle'
| 'rotationX'
| 'rotationY'
| 'scale'
| 'scaleXY'
| 'scaleX'
| 'scaleY'
| 'opacity'
| 'characterSize'
| 'numberEffectProperty'
| 'colorEffectProperty'
| 'objectColor'
| 'objectColorHSL'
| 'objectValue';
declare type TweenInformation = {
type: SceneTweenType | ObjectTweenType;
layerName?: string;
variable?: Variable;
effectName?: string;
propertyName?: string;
scaleFromCenterOfObject?: boolean;
useHSLColorTransition?: boolean;
destroyObjectWhenFinished?: boolean;
};
declare type TweenInformationNetworkSyncData = Omit<
TweenInformation,
'variable' // When synced, a variable is replaced by its path
> & { variablePath?: string[] };
declare type TweenInstanceNetworkSyncData<T> = {
initialValue: T;
targetedValue: T;
elapsedTime: float;
totalDuration: float;
easingIdentifier: string;
interpolationString: 'linear' | 'exponential';
isPaused: boolean;
tweenInformation: TweenInformationNetworkSyncData;
};
declare type TweenManagerNetworkSyncData = {
tweens: Record<
string,
| TweenInstanceNetworkSyncData<float>
| TweenInstanceNetworkSyncData<Array<float>>
>;
};
declare interface GdVersionData {
build: number;
major: number;
@@ -199,6 +312,14 @@ declare interface LayoutNetworkSyncData {
extVar?: {
[extensionName: string]: VariableNetworkSyncData[];
};
time?: TimeManagerSyncData;
tween?: TweenManagerNetworkSyncData;
once?: OnceTriggersSyncData;
layers?: {
[layerName: string]: LayerNetworkSyncData;
};
async?: AsyncTasksManagerNetworkSyncData;
color?: integer;
}
declare interface SceneStackSceneNetworkSyncData {
@@ -208,12 +329,30 @@ declare interface SceneStackSceneNetworkSyncData {
declare type SceneStackNetworkSyncData = SceneStackSceneNetworkSyncData[];
declare type SoundSyncData = {
loop: boolean;
volume: float;
rate: float;
resourceName: string;
seek: float;
};
declare type ChannelsSoundSyncData = Record<integer, SoundSyncData>;
declare type SoundManagerSyncData = {
globalVolume: float;
cachedSpatialPosition: Record<number, [number, number, number]>;
freeSounds: SoundSyncData[];
freeMusics: SoundSyncData[];
musics: ChannelsSoundSyncData;
sounds: ChannelsSoundSyncData;
};
declare interface GameNetworkSyncData {
var?: VariableNetworkSyncData[];
ss?: SceneStackNetworkSyncData;
extVar?: {
[extensionName: string]: VariableNetworkSyncData[];
};
sm?: SoundManagerSyncData;
}
declare interface EventsFunctionsExtensionData {

9
GDJS/Runtime/types/save-state.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare type SceneSaveState = {
sceneData: LayoutNetworkSyncData;
objectDatas: { [objectId: integer]: ObjectNetworkSyncData };
};
declare type GameSaveState = {
gameNetworkSyncData: GameNetworkSyncData;
layoutNetworkSyncDatas: SceneSaveState[];
};

View File

@@ -9,7 +9,7 @@ namespace gdjs {
/**
* Children of a structure.
*/
type Children = Record<string, gdjs.Variable>;
export type Children = Record<string, gdjs.Variable>;
/**
* A Variable is an object storing a value (number or a string) or children variables.
@@ -108,6 +108,171 @@ namespace gdjs {
return target;
}
static getVariableDataFromNetworkSyncData = (
syncData: VariableNetworkSyncData
): VariableData => {
return {
name: syncData.name,
value: syncData.value,
type: syncData.type,
children: syncData.children
? syncData.children.map((childSyncData) =>
gdjs.Variable.getVariableDataFromNetworkSyncData(childSyncData)
)
: undefined,
};
};
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): UnnamedVariableNetworkSyncData | undefined {
const syncedPlayerNumber = syncOptions.playerNumber;
const isHost = syncOptions.isHost;
const variableOwner = this.getPlayerOwnership();
if (
// Variable undefined.
this.isUndefinedInContainer() ||
// If we force sync everything, we don't look at the ownership.
(!syncOptions.syncAllVariables &&
// Variable marked as not to be synchronized.
(variableOwner === null ||
// Getting sync data for a specific player:
(syncedPlayerNumber !== undefined &&
// Variable is owned by host but this player number is not the host.
variableOwner === 0 &&
!isHost) ||
// Variable is owned by a player but not getting sync data for this player number.
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)))
) {
// In those cases, the variable should not be synchronized.
return;
}
const variableType = this.getType();
const variableValue =
variableType === 'structure' || variableType === 'array'
? ''
: this.getValue();
return {
value: variableValue,
type: variableType,
children: this.getStructureNetworkSyncData(this),
owner: variableOwner,
};
}
// Structure variables can contain other variables, so we need to recursively
// get the sync data for each child variable.
getStructureNetworkSyncData(
variable: gdjs.Variable
): VariableNetworkSyncData[] | undefined {
if (variable.getType() === 'array') {
const allVariableNetworkSyncData: VariableNetworkSyncData[] = [];
variable.getAllChildrenArray().forEach((childVariable) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' || childVariableType === 'array'
? ''
: childVariable.getValue();
const childVariableOwner = childVariable.getPlayerOwnership();
if (
// Variable undefined.
childVariable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
childVariableOwner === null
) {
// In those cases, the variable should not be synchronized.
return;
}
allVariableNetworkSyncData.push({
name: '',
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
owner: childVariableOwner,
});
});
return allVariableNetworkSyncData;
}
if (variable.getType() === 'structure') {
const variableChildren = variable.getAllChildren();
if (!variableChildren) return undefined;
const allVariableNetworkSyncData: VariableNetworkSyncData[] = [];
Object.entries(variableChildren).forEach(
([childVariableName, childVariable]) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' || childVariableType === 'array'
? ''
: childVariable.getValue();
const childVariableOwner = childVariable.getPlayerOwnership();
if (
// Variable undefined.
childVariable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
childVariableOwner === null
) {
// In those cases, the variable should not be synchronized.
return;
}
allVariableNetworkSyncData.push({
name: childVariableName,
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
owner: childVariableOwner,
});
}
);
return allVariableNetworkSyncData;
}
return undefined;
}
updateFromNetworkSyncData(
networkSyncData: VariableNetworkSyncData,
options: UpdateFromNetworkSyncDataOptions
) {
// // If we receive an update for this variable for a different owner than the one we know about,
// then 2 cases:
// - If we are the owner of the variable, then ignore the message, we assume it's a late update message or a wrong one,
// we are confident that we own this variable. (it may be reverted if we don't receive an acknowledgment in time)
// - If we are not the owner of the variable, then assume that we missed the ownership change message, so update the variable's
// ownership and then update the variable.
const syncedVariableOwner = networkSyncData.owner;
const variableData =
gdjs.Variable.getVariableDataFromNetworkSyncData(networkSyncData);
if (!options.ignoreVariableOwnership) {
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
const currentVariableOwner = this.getPlayerOwnership();
if (currentPlayerNumber === currentVariableOwner) {
// Variable owned by us, ignoring update message.
return;
}
if (
syncedVariableOwner &&
syncedVariableOwner !== currentVariableOwner
) {
/// Variable owned by someone else on our game, changing ownership as part of the update event.
this.setPlayerOwnership(syncedVariableOwner);
}
}
this.reinitialize(variableData);
}
/**
* Converts a JavaScript object into a value compatible
* with GDevelop variables and store it inside this variable.
@@ -148,7 +313,7 @@ namespace gdjs {
" aren't supported by GDevelop variables, it will be reduced to that size."
);
// @ts-ignore
variable.setNumber(parseInt(obj, 10));
this.setNumber(parseInt(obj, 10));
} else if (typeof obj === 'function') {
logger.error(
'Error while converting JS variable to GDevelop variable: Impossible to set variable value to a function.'

View File

@@ -228,183 +228,89 @@ namespace gdjs {
return null;
}
getVariablePathInContainerByLoopingThroughAllVariables(
variable: gdjs.Variable,
childrenToLookIn: Children | null = null
): string[] | null {
const variables = childrenToLookIn || this._variables.items;
for (const variableName in variables) {
if (variables.hasOwnProperty(variableName)) {
const variableItem = variables[variableName];
if (variableItem === variable) {
return [variableName];
} else if (variableItem.getType() === 'structure') {
const variableItemChildren = variableItem.getAllChildren();
const childPath =
this.getVariablePathInContainerByLoopingThroughAllVariables(
variable,
variableItemChildren
);
if (childPath) {
return [variableName, ...childPath];
}
}
}
}
return null;
}
getVariableFromPath(variablePath: string[]): gdjs.Variable | null {
let variableItems = this._variables.items;
for (let i = 0; i < variablePath.length; i++) {
const part = variablePath[i];
const nextVariable = variableItems[part];
if (!nextVariable) {
return null;
}
if (i === variablePath.length - 1) {
return nextVariable;
}
variableItems = nextVariable.getAllChildren();
}
return null;
}
static _deletedVars: Array<string | undefined> = [];
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): VariableNetworkSyncData[] {
const syncedPlayerNumber = syncOptions.playerNumber;
const isHost = syncOptions.isHost;
const networkSyncData: VariableNetworkSyncData[] = [];
const variableNames = [];
this._variables.keys(variableNames);
variableNames.forEach((variableName) => {
const variable = this._variables.get(variableName);
const variableOwner = variable.getPlayerOwnership();
if (
// Variable undefined.
variable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
variableOwner === null ||
// Getting sync data for a specific player:
(syncedPlayerNumber !== undefined &&
// Variable is owned by host but this player number is not the host.
variableOwner === 0 &&
!isHost) ||
// Variable is owned by a player but not getting sync data for this player number.
(variableOwner !== 0 && syncedPlayerNumber !== variableOwner)
) {
// In those cases, the variable should not be synchronized.
return;
const variableSyncData = variable.getNetworkSyncData(syncOptions);
if (variableSyncData) {
networkSyncData.push({
name: variableName,
...variableSyncData,
});
}
const variableType = variable.getType();
const variableValue =
variableType === 'structure' || variableType === 'array'
? ''
: variable.getValue();
networkSyncData.push({
name: variableName,
value: variableValue,
type: variableType,
children: this.getStructureNetworkSyncData(variable),
owner: variableOwner,
});
});
return networkSyncData;
}
// Structure variables can contain other variables, so we need to recursively
// get the sync data for each child variable.
getStructureNetworkSyncData(
variable: gdjs.Variable
): VariableNetworkSyncData[] | undefined {
if (variable.getType() === 'array') {
const allVariableNetworkSyncData: VariableNetworkSyncData[] = [];
variable.getAllChildrenArray().forEach((childVariable) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' || childVariableType === 'array'
? ''
: childVariable.getValue();
const childVariableOwner = childVariable.getPlayerOwnership();
if (
// Variable undefined.
childVariable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
childVariableOwner === null
) {
// In those cases, the variable should not be synchronized.
return;
}
allVariableNetworkSyncData.push({
name: '',
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
owner: childVariableOwner,
});
});
return allVariableNetworkSyncData;
}
if (variable.getType() === 'structure') {
const variableChildren = variable.getAllChildren();
if (!variableChildren) return undefined;
const allVariableNetworkSyncData: VariableNetworkSyncData[] = [];
Object.entries(variableChildren).forEach(
([childVariableName, childVariable]) => {
const childVariableType = childVariable.getType();
const childVariableValue =
childVariableType === 'structure' || childVariableType === 'array'
? ''
: childVariable.getValue();
const childVariableOwner = childVariable.getPlayerOwnership();
if (
// Variable undefined.
childVariable.isUndefinedInContainer() ||
// Variable marked as not to be synchronized.
childVariableOwner === null
) {
// In those cases, the variable should not be synchronized.
return;
}
allVariableNetworkSyncData.push({
name: childVariableName,
value: childVariableValue,
type: childVariableType,
children: this.getStructureNetworkSyncData(childVariable),
owner: childVariableOwner,
});
}
);
return allVariableNetworkSyncData;
}
return undefined;
}
updateFromNetworkSyncData(networkSyncData: VariableNetworkSyncData[]) {
updateFromNetworkSyncData(
networkSyncData: VariableNetworkSyncData[],
options: UpdateFromNetworkSyncDataOptions
) {
const that = this;
for (let j = 0; j < networkSyncData.length; ++j) {
const variableSyncData = networkSyncData[j];
const variableData =
that._getVariableDataFromNetworkSyncData(variableSyncData);
gdjs.Variable.getVariableDataFromNetworkSyncData(variableSyncData);
const variableName = variableData.name;
if (!variableName) continue;
const variable = that.get(variableName);
// // If we receive an update for this variable for a different owner than the one we know about,
// then 2 cases:
// - If we are the owner of the variable, then ignore the message, we assume it's a late update message or a wrong one,
// we are confident that we own this variable. (it may be reverted if we don't receive an acknowledgment in time)
// - If we are not the owner of the variable, then assume that we missed the ownership change message, so update the variable's
// ownership and then update the variable.
const syncedVariableOwner = variableSyncData.owner;
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
const currentVariableOwner = variable.getPlayerOwnership();
if (currentPlayerNumber === currentVariableOwner) {
console.info(
`Variable ${variableName} is owned by us ${gdjs.multiplayer.playerNumber}, ignoring update message from ${syncedVariableOwner}.`
);
return;
}
if (syncedVariableOwner !== currentVariableOwner) {
console.info(
`Variable ${variableName} is owned by ${currentVariableOwner} on our game, changing ownership to ${syncedVariableOwner} as part of the update event.`
);
variable.setPlayerOwnership(syncedVariableOwner);
}
variable.reinitialize(variableData);
variable.updateFromNetworkSyncData(variableSyncData, options);
}
}
_getVariableDataFromNetworkSyncData(
syncData: VariableNetworkSyncData
): VariableData {
return {
name: syncData.name,
value: syncData.value,
type: syncData.type,
children: syncData.children
? syncData.children.map((childSyncData) =>
this._getVariableDataFromNetworkSyncData(childSyncData)
)
: undefined,
};
}
/**
* "Bad" variable container, used by events when no other valid container can be found.
* This container has no state and always returns the bad variable ( see VariablesContainer.badVariable ).
@@ -440,18 +346,18 @@ namespace gdjs {
updateFromNetworkSyncData: function () {
return;
},
getStructureNetworkSyncData: function () {
return undefined;
},
_getVariableDataFromNetworkSyncData: function () {
return {};
},
hasVariable: function () {
return false;
},
getVariableNameInContainerByLoopingThroughAllVariables: function () {
return '';
},
getVariablePathInContainerByLoopingThroughAllVariables: function () {
return [];
},
getVariableFromPath: function () {
return null;
},
rebuildIndexFrom: function () {
return;
},
@@ -572,6 +478,15 @@ namespace gdjs {
disableSynchronization: function () {
return;
},
getNetworkSyncData: function () {
return undefined;
},
getStructureNetworkSyncData: function () {
return [];
},
updateFromNetworkSyncData: function () {
return;
},
};
}
}

View File

@@ -19,6 +19,7 @@ const allowedExtensions = [
'.map',
'.wasm',
'.txt',
'.png',
];
// These extensions will be built with esbuild (the other will be copied).
@@ -42,6 +43,7 @@ const untransformedPaths = [
'GDJS/Runtime/FacebookInstantGames',
'GDJS/Runtime/libs/CocoonJS',
'GDJS/Runtime/libs/rbush.js',
'GDJS/Runtime/InGameEditor/Resources/primitivedrawingicon.png',
// Extensions pre-built files:
'Extensions/Leaderboards/sha256.js',

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Some files were not shown because too many files have changed in this diff Show More