Skip to content

Commit

Permalink
Add an option to keep layer centered when the game size changes, or k…
Browse files Browse the repository at this point in the history
…eep top-left fixed (as before) (#7188)

* Also fix anchor behavior when used on layers where the camera was moved
  • Loading branch information
4ian authored Nov 26, 2024
1 parent 59685bc commit 1912916
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 33 deletions.
5 changes: 5 additions & 0 deletions Core/GDCore/Project/Layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Camera Layer::badCamera;

Layer::Layer()
: renderingType(""),
defaultCameraBehavior("top-left-anchored-if-never-moved"),
isVisible(true),
isLocked(false),
isLightingLayer(false),
Expand All @@ -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());
Expand Down Expand Up @@ -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));
Expand Down
7 changes: 7 additions & 0 deletions Core/GDCore/Project/Layer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_) {
Expand Down Expand Up @@ -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
Expand Down
43 changes: 25 additions & 18 deletions Extensions/AnchorBehavior/anchorruntimebehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion GDJS/Runtime/RuntimeLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -37,6 +49,7 @@ namespace gdjs {
_name: string;
_renderingType: RuntimeLayerRenderingType;
_cameraType: RuntimeLayerCameraType;
_defaultCameraBehavior: RuntimeLayerDefaultCameraBehavior;
_timeScale: float = 1;
_defaultZOrder: integer = 0;
_hidden: boolean;
Expand Down Expand Up @@ -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 =
Expand Down
30 changes: 25 additions & 5 deletions GDJS/Runtime/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand All @@ -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 &&
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions GDJS/Runtime/types/project-data.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/Bindings/Bindings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/types/gdlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 41 additions & 9 deletions newIDE/app/src/LayersList/LayerEditorDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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>}
Expand Down

0 comments on commit 1912916

Please sign in to comment.