mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
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:
@@ -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));
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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 =
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
1
GDJS/Runtime/types/project-data.d.ts
vendored
1
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -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[];
|
||||
|
@@ -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);
|
||||
|
2
GDevelop.js/types.d.ts
vendored
2
GDevelop.js/types.d.ts
vendored
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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>}
|
||||
|
Reference in New Issue
Block a user