Skip to content

Commit 7d2b575

Browse files
committed
Added camera module to the available sdk modules
1 parent d2999c6 commit 7d2b575

File tree

6 files changed

+456
-12
lines changed

6 files changed

+456
-12
lines changed

engine/modules/camera/pom.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>com.codingame</groupId>
7+
<artifactId>gameengine</artifactId>
8+
<version>master-SNAPSHOT</version>
9+
<relativePath>../../../pom.xml</relativePath>
10+
</parent>
11+
12+
<groupId>com.codingame.gameengine</groupId>
13+
<artifactId>module-camera</artifactId>
14+
<name>CodinGame Game Engine Camera Module</name>
15+
<description>This module allows you to have a camera following some entities</description>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>com.codingame.gameengine</groupId>
20+
<artifactId>core</artifactId>
21+
<version>${project.version}</version>
22+
</dependency>
23+
<dependency>
24+
<groupId>com.codingame.gameengine</groupId>
25+
<artifactId>module-entities</artifactId>
26+
<version>${project.version}</version>
27+
</dependency>
28+
</dependencies>
29+
30+
</project>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package com.codingame.gameengine.module.camera;
2+
3+
import com.codingame.gameengine.core.AbstractPlayer;
4+
import com.codingame.gameengine.core.GameManager;
5+
import com.codingame.gameengine.core.Module;
6+
import com.codingame.gameengine.module.entities.Entity;
7+
import com.codingame.gameengine.module.entities.GraphicEntityModule;
8+
import com.google.inject.Inject;
9+
import com.google.inject.Singleton;
10+
11+
import java.util.ArrayList;
12+
import java.util.Arrays;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
/**
17+
* The CameraModule allow you to have a dynamic camera following a set of objects
18+
*/
19+
@Singleton
20+
public class CameraModule implements Module {
21+
22+
GameManager<AbstractPlayer> gameManager;
23+
@Inject
24+
GraphicEntityModule entityModule;
25+
Map<Integer, Boolean> registered, newRegistration;
26+
ArrayList<Entity<?>> trackedEntities;
27+
Double cameraOffset;
28+
Integer container, sizeX, sizeY = -1;
29+
boolean sentContainer = false;
30+
double previousOffset = -1;
31+
boolean active = true;
32+
boolean oldActive = false;
33+
34+
35+
@Inject
36+
CameraModule(GameManager<AbstractPlayer> gameManager) {
37+
trackedEntities = new ArrayList<>();
38+
this.gameManager = gameManager;
39+
gameManager.registerModule(this);
40+
registered = new HashMap<>();
41+
newRegistration = new HashMap<>();
42+
cameraOffset = 10.;
43+
}
44+
45+
@Override
46+
public void onGameInit() {
47+
sendFrameData();
48+
}
49+
50+
@Override
51+
public void onAfterGameTurn() {
52+
sendFrameData();
53+
}
54+
55+
@Override
56+
public void onAfterOnEnd() {
57+
}
58+
59+
60+
private void sendFrameData() {
61+
Object[] data = {null, null, null, null};
62+
Object[] empty = {null, null, null, null};
63+
if (!newRegistration.isEmpty()) {
64+
data[0] = new HashMap<>(newRegistration);
65+
System.out.printf("added size : %d\n", newRegistration.size());
66+
newRegistration.clear();
67+
}
68+
if (cameraOffset != previousOffset) {
69+
data[1] = cameraOffset;
70+
previousOffset = cameraOffset;
71+
}
72+
if (!sentContainer && container >= 0) {
73+
data[2] = new Integer[]{container, sizeX, sizeY};
74+
sentContainer = true;
75+
}
76+
if (oldActive != active) {
77+
oldActive = active;
78+
data[3] = active;
79+
}
80+
if (!Arrays.equals(data, empty)) {
81+
gameManager.setViewData("c", data);
82+
}
83+
}
84+
85+
private boolean isContainerChild(Entity<?> entity, int _container) {
86+
boolean t = false;
87+
Entity<?> root = entity;
88+
while ((!t) && root.getParent().isPresent()) {
89+
root = root.getParent().get();
90+
t = root.getId() == _container;
91+
}
92+
return t;
93+
}
94+
95+
/**
96+
* Make the camera include the entity in its field of view
97+
*
98+
* @param entity the <code>Entity</code> to add to the tracked entities
99+
*/
100+
public void addTrackedEntity(Entity<?> entity) {
101+
if (isContainerChild(entity, container)) {
102+
int id = entity.getId();
103+
trackedEntities.add(entity);
104+
if (!registered.getOrDefault(id, false)) {
105+
newRegistration.put(id, true);
106+
registered.put(id, true);
107+
System.out.printf("registered %d\n", id);
108+
}
109+
} else {
110+
throw new RuntimeException("The entity given can't be track because it's not the child of " +
111+
"the container / on of the container child !\n" +
112+
"Don't forget to init the camera with the setContainer method");
113+
}
114+
}
115+
116+
/**
117+
* @param entity the <code>Entity</code> that you want to know if it's tracked
118+
* @return if the entity is tracked by the camera or not
119+
*/
120+
public Boolean isTracked(Entity<?> entity) {
121+
return registered.getOrDefault(entity.getId(), false);
122+
}
123+
124+
/**
125+
* Make the camera stop tracking this entity
126+
*
127+
* @param entity the <code>Entity</code> that you don't want to be tracked anymore
128+
*/
129+
public void removeTrackedEntity(Entity<?> entity) {
130+
int id = entity.getId();
131+
trackedEntities.remove(entity);
132+
if (registered.getOrDefault(id, false)) {
133+
newRegistration.put(id, false);
134+
registered.remove(id);
135+
}
136+
}
137+
138+
/**
139+
* Sets the camera offset to the given value. It's the length in pixel between the edge of the screen and the
140+
* closest to border entity
141+
*
142+
* @param value the new camera offset, a positive double
143+
*/
144+
public void setCameraOffset(double value) {
145+
cameraOffset = value;
146+
}
147+
148+
/**
149+
* Initialize the camera with container which has to contain all the other entities tracked by the camera
150+
*
151+
* @param container the <code>Entity</code> to set as the container
152+
* @param viewerSizeX the x size (in pixel) in the viewer of the smallest rectangle that could include your container if the scale is 1
153+
* @param viewerSizeY the y size (in pixel) in the viewer of the smallest rectangle that could include your container if the scale is 1
154+
*/
155+
public void setContainer(Entity<?> container, int viewerSizeX, int viewerSizeY) {
156+
if (trackedEntities.stream().allMatch((Entity<?> e) -> isContainerChild(e, container.getId()))) {
157+
this.container = container.getId();
158+
this.sizeX = viewerSizeX;
159+
this.sizeY = viewerSizeY;
160+
} else {
161+
throw new RuntimeException("You can't change the container if there are tracked that are not child of the new container");
162+
}
163+
}
164+
165+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import {WIDTH, HEIGHT} from '../core/constants.js'
2+
import {api as entityModule} from '../entity-module/GraphicEntityModule.js'
3+
import {easeOut} from '../core/transitions.js'
4+
import {lerpPosition} from '../core/utils.js'
5+
6+
export class CameraModule {
7+
constructor(assets) {
8+
CameraModule.instance = this
9+
this.container = {id: -1, sizeX: -1, sizeY: -1}
10+
this.cameraOffset = 0
11+
this.previousFrame = {
12+
registered: new Map()
13+
}
14+
this.lastFrame = -1
15+
this.cameraEndPosition = {x: 0, y: 0}
16+
this.cameraEndScale = 1
17+
this.cameraCurve = t => t
18+
this.oldZoomState = {position: {x: 0, y: 0}, boundSize: {x: 0, y: 0}}
19+
this.oldCameraState = {scale: {x: -1, y: -1}, position: {x: 0, y: 0}}
20+
this.currentCameraState = {scale: {x: -1, y: -1}, position: {x: 0, y: 0}}
21+
this.previousUpdateData = this.currentUpdateFrame = this.currentUpdateProgress = undefined
22+
this.viewerActive = true
23+
this.active = true
24+
25+
}
26+
27+
static get name() {
28+
return 'c'
29+
}
30+
31+
setActive(active) {
32+
this.viewerActive = active
33+
if (this.currentUpdateProgress !== undefined) {
34+
this.lastFrame = -1
35+
this.updateScene(this.previousUpdateData, this.currentUpdateFrame, this.currentUpdateProgress)
36+
}
37+
}
38+
39+
getRelativePosFromContainer(entity, containerId) {
40+
let x = 0
41+
let y = 0
42+
let root = entity
43+
let debug = 0
44+
while (root.parent !== null && root.id !== containerId) {
45+
x += root.currentState.x
46+
y += root.currentState.y
47+
root = root.parent
48+
debug++
49+
if (debug > 10) {
50+
throw new Error("this is too long")
51+
}
52+
}
53+
return {x, y}
54+
}
55+
56+
updateScene(previousData, currentData, progress) {
57+
const isActive = (currentData.registered.size !== 0) && (currentData.container.entity !== null)
58+
if (!(currentData.active && this.viewerActive)) {
59+
if (isActive) {
60+
currentData.container.entity.graphics.scale = {x: 1, y: 1}
61+
currentData.container.entity.graphics.position = {x: 0, y: 0}
62+
}
63+
return
64+
}
65+
this.currentUpdateFrame = currentData
66+
this.currentUpdateProgress = progress
67+
this.previousUpdateData = previousData
68+
if (this.lastFrame !== currentData.number) {
69+
this.lastFrame = currentData.number
70+
if (isActive) {
71+
this.oldCameraState = {...this.currentCameraState}
72+
let maxX, minX, minY, maxY;
73+
let first = true;
74+
entityModule.entities.forEach(
75+
entity => {
76+
77+
if (currentData.registered.get(entity.id + "")) {
78+
//console.log(`added entity ${entity.id} which is at x = ${entity.currentState.x}, y = ${entity.currentState.y}`)
79+
const relativePos = this.getRelativePosFromContainer(entity, currentData.container.entity.id)
80+
if (first) {
81+
minX = maxX = relativePos.x
82+
minY = maxY = relativePos.y
83+
first = false
84+
} else {
85+
minX = Math.min(minX, relativePos.x)
86+
minY = Math.min(minY, relativePos.y)
87+
maxX = Math.max(maxX, relativePos.x)
88+
maxY = Math.max(maxY, relativePos.y)
89+
}
90+
91+
}
92+
}
93+
)
94+
const averagePoint = {x: (maxX + minX) / 2, y: (maxY + minY) / 2}
95+
const boundSize = {x: maxX - minX, y: maxY - minY}
96+
// if (this.oldCameraState.scale.x !== -1 && progress !== 1) {
97+
// currentData.container.entity.graphics.position = this.oldCameraState.position
98+
// currentData.container.entity.graphics.scale = this.oldCameraState.scale
99+
//
100+
// }
101+
const containerState = currentData.container.entity.currentState
102+
const scale2 = Math.min(HEIGHT / (boundSize.y + currentData.cameraOffset), WIDTH / (boundSize.x + currentData.cameraOffset))
103+
const scale = {x: scale2 / containerState.scaleX, y: scale2 / containerState.scaleY}
104+
//const scale = 1
105+
this.cameraEndScale = scale
106+
107+
// if position is not relative del container.entity.x
108+
const newX = ((currentData.container.sizeX / 2 - averagePoint.x) * scale2
109+
- (scale2 - 1) * currentData.container.sizeX / 2
110+
+ (WIDTH / 2 - (containerState.x + currentData.container.sizeX / 2))) / containerState.scaleX
111+
112+
const newY = ((currentData.container.sizeY / 2 - averagePoint.y) * scale2
113+
- (scale2 - 1) * currentData.container.sizeY / 2
114+
+ (HEIGHT / 2 - (containerState.y + currentData.container.sizeY / 2))) / containerState.scaleY
115+
116+
// currentData.container.entity.graphics.scale.x = currentData.container.entity.graphics.scale.y = 0.5
117+
this.cameraEndPosition = {x: newX, y: newY}
118+
//console.log(`frame ${currentData.number}, ${Math.round(progress*100)/100}%,container to x : ${newX}, y : ${newY}, scale : ${scale}`)
119+
const position = averagePoint
120+
this.cameraCurve = (position.x - this.oldZoomState.position.x) ** 2 +
121+
(position.y - this.oldZoomState.position.y) ** 2 >= currentData.cameraOffset ** 2
122+
|| Math.max(Math.abs(boundSize.x - this.oldZoomState.boundSize.x),
123+
Math.abs(boundSize.y - this.oldZoomState.boundSize.y)) > currentData.cameraOffset ? easeOut : t => t
124+
this.oldZoomState = {boundSize, position}
125+
}
126+
127+
}
128+
if ((this.lastFrame === currentData.number || progress === 1) && isActive) {
129+
const currentPoint = lerpPosition(this.oldCameraState.position, this.cameraEndPosition, this.cameraCurve(progress))
130+
currentData.container.entity.graphics.position = currentPoint
131+
const currentScale = lerpPosition(this.oldCameraState.scale, this.cameraEndScale, this.cameraCurve(progress))
132+
currentData.container.entity.graphics.scale = currentScale
133+
console.log(`frame ${currentData.number}, ${Math.round(progress * 100) / 100}%,container to x : ${currentPoint.x}, y : ${currentPoint.y}, scale : ${currentScale.x}`)
134+
this.currentCameraState = {scale: currentScale, position: currentPoint}
135+
136+
}
137+
}
138+
139+
handleFrameData(frameInfo, data) {
140+
if (data === undefined) {
141+
const registered = new Map(this.previousFrame.registered)
142+
const cameraOffset = this.cameraOffset
143+
const container = this.container.id !== -1 ? {
144+
entity: entityModule.entities.get(this.container.id),
145+
sizeX: this.container.sizeX, sizeY: this.container.sizeY
146+
} : null
147+
const active = this.active
148+
const frame = {registered, number: frameInfo.number, cameraOffset, container, active}
149+
this.previousFrame = frame
150+
return frame
151+
}
152+
// const newRegistration = data[0] === undefined ? new Map() : data[0]
153+
const newRegistration = data[0] || new Map()
154+
const registered = new Map(this.previousFrame.registered)
155+
Object.keys(newRegistration).forEach(
156+
(k) => {
157+
registered.set(k, newRegistration[k])
158+
}
159+
)
160+
this.cameraOffset = data[1] || this.cameraOffset
161+
this.container = data[2] ? {id: data[2][0], sizeX: data[2][1], sizeY: data[2][2]} : this.container
162+
163+
const active = data[3] === null ? this.active : data[3]
164+
this.active = active
165+
const cameraOffset = this.cameraOffset
166+
const container = this.container.id !== -1 ? {
167+
entity: entityModule.entities.get(this.container.id),
168+
sizeX: this.container.sizeX, sizeY: this.container.sizeY
169+
} : null
170+
const frame = {registered, number: frameInfo.number, cameraOffset, container, active}
171+
this.previousFrame = frame
172+
return frame
173+
}
174+
175+
reinitScene() {
176+
177+
}
178+
179+
}

0 commit comments

Comments
 (0)