1. 레이아웃(Layout) 생성
GameScene에도 똑같이 기본 캔버스 아래에 배경 이미지를 넣어준다.
그리고 물고기들이 참조할 영역(어디를 기준으로 +5 높이에서 떨어지고, 어디를 기준으로 아래로 내려가면 사라지는지)이 필요한데,
이를 위해 Layout 노드를 만들어준다. 레이아웃은 기본으로 스프라이트 컴포넌트를 가지고 있는데
배경은 필요 없으므로 스프라이트 컴포넌트는 비활성화하거나 지워준다. 사이즈도 캔버스와 동일한 크기로 설정해준다.
원래 레이아웃은 자식 노드들을 규칙성을 가지고 정렬할 때 (ex. 인벤토리의 아이템들 정렬) 사용하는 노드다.
Type은 이 레이아웃 아래 자식들의 정렬 방식을 뜻한다.
Resize Mode는 부모 캔버스의 사이즈가 바뀔 때, 이 레이아웃도 사이즈를 변경하는데
컨테이너(레이아웃)의 크기를 기준으로 변경할지 자식들의 사이즈를 기준으로 변경할지에 대한 속성이다.
두 속성을 NONE으로 설정하면 컨테이너와 자식이 독립적이고, 자식들 간에 규칙성이 없는 레이아웃이 된다.
이를 통해 레이아웃을 유니티의 RectTransform 또는 Panel과 비슷하게 사용할 수 있다.
2. Properties 뷰를 통해 노드 참조하기
// GameScene.ts
const {ccclass, property} = cc._decorator;
@ccclass
export default class GameScene extends cc.Component {
@property(cc.Layout)
fishLayout:cc.Layout = null;
@property({type:cc.Prefab})
fishPrefabs:Array<cc.Prefab> = [];
}
TitleScene 스크립트와 마찬가지로 Node Tree 뷰에 빈 오브젝트를 만들고 GameScene 스크립트를 붙여준다.
그리고 위에서 만든 레이아웃과 물고기 프리팹 리스트를 스크립트에서 참조하려면 Properies 뷰에 노출시켜서 링크시켜야 한다.
유니티는 변수 앞에 public 이나 [serializefield] 키워드를 붙여서 노출시켰던 것처럼 @propery 키워드가 그 역할을 한다.
물고기들의 부모 노드가 될 레이아웃, 생성할 물고기들의 프리팹 리스트를 연결해준다.
Array의 경우 여러 노드를 선택한 채로 드래그하면 한 번에 넣어진다.
3. 오브젝트 풀링
fishPool:Array<Array<cc.Node>> = [];
start() {
this.InitFishPool();
}
InitFishPool()
{
for (var i = 0; i < this.fishPrefabs.length; i++)
{
var pool = [];
for (var j = 0; j < 10; j++)
{
var fish = cc.instantiate(this.fishPrefabs[i]);
fish.active = false;
this.fishLayout.node.addChild(fish);
pool.push(fish);
}
this.fishPool.push(pool);
}
}
GetFish(idx:number):cc.Node
{
var targetPool = this.fishPool[idx];
for (var i = 0; i < targetPool.length; i++)
{
if (!targetPool[i].active)
{
targetPool[i].active = true;
return targetPool[i];
}
}
}
InitFishPool은 각 물고기 종류별로 10마리씩 생성하고 오브젝트 풀에 추가하는 함수다. 비활성화 상태로 Fish 레이아웃 아래에 생성한다.
GetFish는 물고기 종류(=오브젝트 풀에서의 인덱스)를 파라미터로 받은 뒤 오브젝트 풀에서 꺼내서 반환한다.
표현은 꺼내고 반환한다고 하지만 오브젝트 풀에서 물고기의 추가, 삭제가 이뤄지는 것은 아니다.
위의 코드처럼 오브젝트 풀링을 하면 문제가 발생할 여지가 있다.
바로 targetPool에서 active가 false인 물고기를 찾지 못해서 리턴 값이 없을 때이다.
GetFish 함수가 실행되는 타이밍에 targetPool의 모든 물고기들의 active가 true인 상황을 뜻한다.
물고기가 화면 밖으로 떨어지면 active를 false로 초기화(오브젝트 풀에 반납)해야한다.
지금처럼 아직 반납하는 코드를 작성하지 못했거나 작성한 반납 코드가 제대로 작동하지 않고
오브젝트 풀에 반납되기 전에 물고기를 너무 빠른 간격으로 반복해서 꺼내면 문제가 발생할 수 있다.
이 부분에 대해서는 따로 예외 처리하지는 않았지만...
GetFish(idx:number):cc.Node
{
var targetPool = this.fishPool[idx];
for (var i = 0; i < targetPool.length; i++)
{
if (!targetPool[i].active)
{
targetPool[i].active = true;
return targetPool[i];
}
}
var fish = cc.instantiate(this.fishPrefabs[i]);
fish.active = false;
this.fishLayout.node.addChild(fish);
targetPool.push(fish);
return fish;
}
초당 수 백발의 총알을 발사하는 슈팅 게임에서는 이런 식으로 방어 코드를 추가해도 될 것 같다.
리턴할 오브젝트를 못 찾았을 때 해당 풀에 새 오브젝트를 추가하는 것이다. 물론 이것도 완벽한 해결책은 아니겠지만.
4. 물고기 스폰
위에서 작성한 물고기 오브젝트 풀을 이용해 물고기들을 랜덤 하게 떨어트린다.
스폰 위치(X축), 스폰 쿨타임, 스폰할 물고기 종류, 내려오는 속도. 4가지 변수에 랜덤을 적용한다.
그리고 물고기가 화면 아래로 사라지면 오브젝트 풀에 반납한다.
spawnDelta:number;
spawnCoolTime:number;
update(dt) {
this.SpawnFish(dt);
}
SpawnFish(dt : number)
{
// 스폰 쿨타임 적용
this.spawnDelta += dt;
if (this.spawnDelta < this.spawnCoolTime) return;
// 물고기 오브젝트 풀에서 랜덤한 피쉬 꺼내오기
var fishNum = Math.floor(Math.random() * this.fishPrefabs.length);
var fish = this.GetFish(fishNum);
// 물고기의 시작 좌표를 랜덤한 위치로 초기화
fish.setPosition(this.GetRandomPosition());
// 물고기의 이동 함수(Action)와 종료 함수(Callback)
var move = cc.moveBy(Math.random() * 2 + 1.5, 0, -this.fishLayout.node.getContentSize().height * 1.2);
var finish = cc.callFunc(() => {
fish.stopAllActions();
fish.active = false;
});
// 시퀀스 실행
var sequence = cc.sequence(move, finish);
fish.runAction(sequence);
// 스폰 쿨타임 초기화
this.spawnDelta = 0;
this.spawnCoolTime = Math.random() + 0.4;
}
GetRandomPosition() : cc.Vec2
{
var positionRange = this.fishLayout.node.getContentSize();
var x = (positionRange.width * Math.random()) - (positionRange.width * 0.5);
var y = positionRange.height * 0.6;
return new cc.Vec2(x, y);
}
(1) this.fishLayout.node.getContentSize()
물고기의 스폰 X좌표를 계산할 때 this.fishLayout.node.getContentSize().width * 0.5 를 쓴다거나
물고기의 소멸 Y좌표를 계산할 때 this.fishLayout.node.getContentSize().height * 1.2 이런 식으로 쓴 것처럼
레이아웃의 비율을 통해서 물고기의 좌표를 설정했다.
FishLayout에 물고기 하나를 놓아보면 (0,0) 좌표는 화면 중앙이라는 것을 알 수 있다.
(-width * 0.5, height * 0.5) 좌표는 레이아웃의 왼쪽 상단 모서리를 가리킨다.
물고기의 현재 포지션 y 값에 height * 1 만큼 빼주면, 화면의 높이만큼 물고기가 아래로 내려가는 것이다.
(2) Action 사용
물고기를 아래로 이동시키는 move 액션과 move가 끝나면 실행되는 콜백 형식의 finish 액션을 만든다.
그리고 두 액션을 순차적으로 실행시켜주는 시퀀스를 만들고, 물고기 노드가 그 시퀀스를 실행하도록 하는 코드다.
코코스 크리에이터 레퍼런스에 의하면 액션 시스템은 점진적으로 없앨 것이며 cc.tween을 권장한다고 한다.
var startPos = fish.position;
var targetPos = new cc.Vec3(startPos.x, -this.fishLayout.node.getContentSize().height * 1.2, startPos.z);
var duration = Math.random() * 2 + 1.5;
cc.tween(fish)
.to(duration, {position:targetPos})
.call(() => {
fish.stopAllActions();
fish.active = false;
})
.start();
액션과 시퀀스 대신에 cc.tween을 사용한다면 똑같은 작업을 위와 같이 대체할 수 있을 것 같다.
cc.tween에 콜백 액션을 추가하는 call 함수가 있는 것처럼 tween에 다른 액션이나 시퀀스를 추가할 수 있어서 혼용해서 사용해도 될 것 같다.
(3) Math.Random
// 물고기 오브젝트 풀에서 랜덤한 피쉬 꺼내오기
var fishNum = Math.floor(Math.random() * this.fishPrefabs.length);
var fish = this.GetFish(fishNum);
어떤 리스트에서 무작위 요소를 꺼내는 일은 대부분의 게임 프로젝트에서 빈번하게 수행하는 작업일 것이다.
예를 들어, 길이가 5인 리스트가 있으면 랜덤 인덱스(0 ~ 4)로 접근해서 구현할 수 있다.
코코스 크리에이터에서는 유니티처럼 자체적인 난수 생성 API를 제공하지는 않고 있다.
자바스크립트의 난수 생성 함수인 Math.random 이나 window.crypto.getRandomValues 함수를 사용해야한다.
(둘 간의 비교는 구글링하면 많은 자료가 나온다)
Math.random 함수는 0 이상 1 미만의 실수를 반환하는데, 이 값에 기대하는 랜덤 값 범위(range)를 곱해주는 것으로 처리했다.
cc.randomRange와 cc.randomRangeInt 함수가 문서에 있는 것을 보면
코코스 크리에이터도 유니티와 비슷하게 함수를 제공하려 했던 것 같지만 현재는 사용할 수 없다.
함수 구현이 없어서 Uncaught TypeError가 뜬다. (사용할 수 없는 함수를 보여주는 것도 버그라고 생각한다..)
결국 0 부터 1 까지의 실수가 나오는 Math.random 함수 하나 가지고서 사용자가 기능을 확장해야 하는데
이런 부분들이 아직까지는 유니티에 비해 부족하다고 생각이 든다.
'Cocos Creator' 카테고리의 다른 글
[예제 게임] GameScene #3 (게임 일시 정지) (0) | 2021.06.03 |
---|---|
[예제 게임] GameScene #2 (UI 배치, 클릭 이벤트, 스코어 갱신) (0) | 2021.06.01 |
[예제 게임] 씬 전환과 버튼 클릭 이벤트 (0) | 2021.05.31 |
[예제 게임] TitleScene 만들기 (해상도와 UI) (0) | 2021.05.31 |
개발 환경 갖추기 (0) | 2021.05.28 |