Add an option to keep layer centered when the game size changes, or keep top-left fixed (as before) (#7188)

* Also fix anchor behavior when used on layers where the camera was moved
This commit is contained in:
Florian Rival
2024-11-26 15:41:11 +01:00
committed by GitHub
parent 59685bc4c4
commit 1912916778
10 changed files with 127 additions and 33 deletions

View File

@@ -15,6 +15,7 @@ Camera Layer::badCamera;
Layer::Layer()
: renderingType(""),
defaultCameraBehavior("top-left-anchored-if-never-moved"),
isVisible(true),
isLocked(false),
isLightingLayer(false),
@@ -40,6 +41,9 @@ void Layer::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", GetName());
element.SetAttribute("renderingType", GetRenderingType());
element.SetAttribute("cameraType", GetCameraType());
if (GetDefaultCameraBehavior() != "top-left-anchored-if-never-moved") {
element.SetAttribute("defaultCameraBehavior", GetDefaultCameraBehavior());
}
element.SetAttribute("visibility", GetVisibility());
element.SetAttribute("isLocked", IsLocked());
element.SetAttribute("isLightingLayer", IsLightingLayer());
@@ -80,6 +84,7 @@ void Layer::UnserializeFrom(const SerializerElement& element) {
SetName(element.GetStringAttribute("name", "", "Name"));
SetRenderingType(element.GetStringAttribute("renderingType", ""));
SetCameraType(element.GetStringAttribute("cameraType", "perspective"));
SetDefaultCameraBehavior(element.GetStringAttribute("defaultCameraBehavior", "top-left-anchored-if-never-moved"));
SetVisibility(element.GetBoolAttribute("visibility", true, "Visibility"));
SetLocked(element.GetBoolAttribute("isLocked", false));
SetLightingLayer(element.GetBoolAttribute("isLightingLayer", false));

View File

@@ -109,6 +109,12 @@ class GD_CORE_API Layer {
renderingType = renderingType_;
}
const gd::String& GetDefaultCameraBehavior() const { return defaultCameraBehavior; }
void SetDefaultCameraBehavior(const gd::String& defaultCameraBehavior_) {
defaultCameraBehavior = defaultCameraBehavior_;
}
const gd::String& GetCameraType() const { return cameraType; }
void SetCameraType(const gd::String& cameraType_) {
@@ -275,6 +281,7 @@ class GD_CORE_API Layer {
gd::String name; ///< The name of the layer
gd::String renderingType; ///< The rendering type: "" (empty), "2d", "3d" or
///< "2d+3d".
gd::String defaultCameraBehavior;
gd::String cameraType;
bool isVisible; ///< True if the layer is visible
bool isLocked; ///< True if the layer is locked

View File

@@ -110,13 +110,15 @@ namespace gdjs {
}
//Calculate the distances from the window's bounds.
const topLeftPixel = this._convertCoords(
instanceContainer,
layer,
this.owner.getDrawableX(),
this.owner.getDrawableY(),
workingPoint
);
const topLeftPixel = this._relativeToOriginalWindowSize
? [this.owner.getDrawableX(), this.owner.getDrawableY()]
: this._convertInverseCoords(
instanceContainer,
layer,
this.owner.getDrawableX(),
this.owner.getDrawableY(),
workingPoint
);
// Left edge
if (this._leftEdgeAnchor === HorizontalAnchor.WindowLeft) {
@@ -141,13 +143,18 @@ namespace gdjs {
}
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
const bottomRightPixel = this._convertCoords(
instanceContainer,
layer,
this.owner.getDrawableX() + this.owner.getWidth(),
this.owner.getDrawableY() + this.owner.getHeight(),
workingPoint
);
const bottomRightPixel = this._relativeToOriginalWindowSize
? [
this.owner.getDrawableX() + this.owner.getWidth(),
this.owner.getDrawableY() + this.owner.getHeight(),
]
: this._convertInverseCoords(
instanceContainer,
layer,
this.owner.getDrawableX() + this.owner.getWidth(),
this.owner.getDrawableY() + this.owner.getHeight(),
workingPoint
);
// Right edge
if (this._rightEdgeAnchor === HorizontalAnchor.WindowLeft) {
@@ -226,17 +233,17 @@ namespace gdjs {
}
// It's fine to reuse workingPoint as topLeftPixel is no longer used.
const topLeftCoord = this._convertInverseCoords(
const topLeftCoord = this._convertCoords(
instanceContainer,
layer,
leftPixel,
topPixel,
workingPoint
);
const left = topLeftCoord[0];
const top = topLeftCoord[1];
let left = topLeftCoord[0];
let top = topLeftCoord[1];
const bottomRightCoord = this._convertInverseCoords(
const bottomRightCoord = this._convertCoords(
instanceContainer,
layer,
rightPixel,

View File

@@ -23,11 +23,23 @@ namespace gdjs {
PERSPECTIVE,
ORTHOGRAPHIC,
}
const getCameraTypeFromString = (renderingTypeAsString: string | undefined) =>
renderingTypeAsString === 'orthographic'
? RuntimeLayerCameraType.ORTHOGRAPHIC
: RuntimeLayerCameraType.PERSPECTIVE;
export enum RuntimeLayerDefaultCameraBehavior {
DO_NOTHING,
TOP_LEFT_ANCHORED_IF_NEVER_MOVED,
}
const getDefaultCameraBehaviorFromString = (
defaultCameraBehaviorAsString: string
) =>
defaultCameraBehaviorAsString === 'top-left-anchored-if-never-moved'
? RuntimeLayerDefaultCameraBehavior.TOP_LEFT_ANCHORED_IF_NEVER_MOVED
: RuntimeLayerDefaultCameraBehavior.DO_NOTHING;
/**
* Represents a layer of a "container", used to display objects.
* The container can be a scene (see gdjs.Layer)
@@ -37,6 +49,7 @@ namespace gdjs {
_name: string;
_renderingType: RuntimeLayerRenderingType;
_cameraType: RuntimeLayerCameraType;
_defaultCameraBehavior: RuntimeLayerDefaultCameraBehavior;
_timeScale: float = 1;
_defaultZOrder: integer = 0;
_hidden: boolean;
@@ -70,6 +83,9 @@ namespace gdjs {
this._name = layerData.name;
this._renderingType = getRenderingTypeFromString(layerData.renderingType);
this._cameraType = getCameraTypeFromString(layerData.cameraType);
this._defaultCameraBehavior = getDefaultCameraBehaviorFromString(
layerData.defaultCameraBehavior || 'top-left-anchored-if-never-moved'
);
this._hidden = !layerData.visibility;
this._initialCamera3DFieldOfView = layerData.camera3DFieldOfView || 45;
this._initialCamera3DNearPlaneDistance =

View File

@@ -28,8 +28,24 @@ namespace gdjs {
) {
super(layerData, instanceContainer);
this._cameraX = this.getWidth() / 2;
this._cameraY = this.getHeight() / 2;
if (
this._defaultCameraBehavior ===
gdjs.RuntimeLayerDefaultCameraBehavior.TOP_LEFT_ANCHORED_IF_NEVER_MOVED
) {
// If top-left must stay in the top-left corner, this means we center the camera on the current size.
this._cameraX = this._runtimeScene.getViewportOriginX();
this._cameraY = this._runtimeScene.getViewportOriginY();
} else {
// Otherwise, the default camera position is the center of the initial viewport.
this._cameraX =
(this._runtimeScene.getInitialUnrotatedViewportMinX() +
this._runtimeScene.getInitialUnrotatedViewportMaxX()) /
2;
this._cameraY =
(this._runtimeScene.getInitialUnrotatedViewportMinY() +
this._runtimeScene.getInitialUnrotatedViewportMaxY()) /
2;
}
if (this.getCameraType() === gdjs.RuntimeLayerCameraType.ORTHOGRAPHIC) {
this._cameraZ =
(this._initialCamera3DFarPlaneDistance +
@@ -53,10 +69,13 @@ namespace gdjs {
// * When the camera follows a player/object, it will rarely be at the default position.
// (and if is, it will be moved again by the behavior/events).
// * Cameras not following a player/object are usually UIs which are intuitively
// expected not to "move". Not adapting the center position would make the camera
// move from its initial position (which is centered on the screen) - and anchor
// behavior would behave counterintuitively.
// expected not to "move" (top-left stays "fixed"), while gameplay is "centered" (center stays "fixed").
//
// Note that anchor behavior is usually a better choice for UIs.
if (
this._defaultCameraBehavior ===
gdjs.RuntimeLayerDefaultCameraBehavior
.TOP_LEFT_ANCHORED_IF_NEVER_MOVED &&
// Have a safety margin of 1 pixel to avoid rounding errors.
Math.abs(this._cameraX - oldGameResolutionOriginX) < 1 &&
Math.abs(this._cameraY - oldGameResolutionOriginY) < 1 &&
@@ -352,6 +371,7 @@ namespace gdjs {
y *= Math.abs(this._zoomFactor);
position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2;
position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2;
return position;
}

View File

@@ -283,6 +283,7 @@ declare interface LayerData {
name: string;
renderingType?: '' | '2d' | '3d' | '2d+3d';
cameraType?: 'perspective' | 'orthographic';
defaultCameraBehavior?: 'top-left-anchored-if-never-moved' | 'do-nothing';
visibility: boolean;
cameras: CameraData[];
effects: EffectData[];

View File

@@ -1050,6 +1050,8 @@ interface Layer {
[Const, Ref] DOMString GetRenderingType();
void SetCameraType([Const] DOMString cameraType);
[Const, Ref] DOMString GetCameraType();
void SetDefaultCameraBehavior([Const] DOMString defaultCameraBehavior);
[Const, Ref] DOMString GetDefaultCameraBehavior();
void SetVisibility(boolean visible);
boolean GetVisibility();
void SetLocked(boolean isLocked);

View File

@@ -867,6 +867,8 @@ export class Layer extends EmscriptenObject {
getRenderingType(): string;
setCameraType(cameraType: string): void;
getCameraType(): string;
setDefaultCameraBehavior(defaultCameraBehavior: string): void;
getDefaultCameraBehavior(): string;
setVisibility(visible: boolean): void;
getVisibility(): boolean;
setLocked(isLocked: boolean): void;

View File

@@ -7,6 +7,8 @@ declare class gdLayer {
getRenderingType(): string;
setCameraType(cameraType: string): void;
getCameraType(): string;
setDefaultCameraBehavior(defaultCameraBehavior: string): void;
getDefaultCameraBehavior(): string;
setVisibility(visible: boolean): void;
getVisibility(): boolean;
setLocked(isLocked: boolean): void;

View File

@@ -21,10 +21,11 @@ import HotReloadPreviewButton, {
import HelpButton from '../UI/HelpButton';
import { Tabs } from '../UI/Tabs';
import EffectsList from '../EffectsList';
import { Spacer } from '../UI/Grid';
import { Column, Line, Spacer } from '../UI/Grid';
import SemiControlledTextField from '../UI/SemiControlledTextField';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
import Paper from '../UI/Paper';
const gd: libGDevelop = global.gd;
@@ -237,20 +238,51 @@ const LayerEditorDialog = ({
</Trans>
</DismissableAlertMessage>
) : null}
<Text size="block-title">
<Trans>Camera positioning</Trans>
</Text>
<SelectField
fullWidth
floatingLabelText={<Trans>Default camera behavior</Trans>}
value={layer.getDefaultCameraBehavior()}
onChange={(e, i, value: string) => {
layer.setDefaultCameraBehavior(value);
forceUpdate();
}}
>
<SelectOption
value={'do-nothing'}
label={t`Keep centered (best for game content)`}
/>
<SelectOption
value={'top-left-anchored-if-never-moved'}
label={t`Keep top-left corner fixed (best for content that can extend)`}
/>
</SelectField>
<Text size="block-title">
<Trans>Visibility and instances ordering</Trans>
</Text>
<Text>
<Trans>
There are {instancesCount} instances of objects on this layer.
</Trans>
</Text>
{!project.getUseDeprecatedZeroAsDefaultZOrder() && (
<Text>
<Trans>
Objects created using events on this layer will be given a "Z
order" of {highestZOrder + 1}, so that they appear in front of
all objects of this layer. You can change this using the action
to change an object Z order, after using an action to create it.
</Trans>
</Text>
<Paper background="light" variant="outlined">
<Line>
<Column>
<Text noMargin>
<Trans>
Objects created using events on this layer will be given a
"Z order" of {highestZOrder + 1}, so that they appear in
front of all objects of this layer. You can change this
using the action to change an object Z order, after using
an action to create it.
</Trans>
</Text>
</Column>
</Line>
</Paper>
)}
<InlineCheckbox
label={<Trans>Hide the layer</Trans>}