Virtual Church Of The Digital Goddess is my entry for this 55th LudumDare session. When I discovered the chosen theme (Summoning) I was in an uncomfortable situation. By religious conviction I cannot play with black magical art, evil spirits and all things more or less similar. I have immediately understood that a big amount of entries could play with sorcery - and LD members did. I respect this choice, but I tried to create something different without evil invocation, black sabbath, devil, etc. This is the first time in my indie dev life that I encountered this constraint. So I struggled with a new challenge. Let's go!
I decided to create (another) puzzle game - my favorite games' genre, but adding a new way. I asked myself - is there a way to create a game only with a mathematical core, no rigidbody, no collider, etc? So VCDG is an experimentation into solid math processes. Maybe for this reason, the cubes and their movements seem so smooth, perfectly aligned on edges. To do that I use this code :
```
// smooth rotation
IEnumerator Tumble(GameObject thisOne, Vector3 direction, Vector3 endPosition, Quaternion endRotation, Vector3 pivot, Vector3 rotAxis, float rotSpeed, bool ret)
{
float t = 0.0f;
if (loader.GetComponent<Loader>()._move != null && loader != null)
{
if (Time.timeScale > 1.5f)
_loader.playSound(_loader._move, this.transform, true, 0.21f, 0.41f, _loader._move.length, false);
else
_loader.playSound(_loader._move, this.transform, true, 0.31f, UnityEngine.Random.Range(0.71f, 0.92f), _loader._move.length, false);
}
// rotate player cube
while (t < tumblingDuration)
{
isTumbling = true; // unreachable
t += Time.deltaTime;
// tip to avoid tiny glitch
if (Mathf.Abs(t - tumblingDuration) < tumblingDuration / 30)
t += 1.0f; // out of the loop right now
thisOne.transform.RotateAround(pivot, rotAxis, rotSpeed * Time.deltaTime);
yield return Timing.WaitForOneFrame;
}
// clean position and rotation
thisOne.transform.position = endPosition;
thisOne.transform.rotation = endRotation;
// store new position so as to check forbidden face
storedPos = endPosition - shift;
// don't store the return movement in back in time mode!!!
if (!ret)
addNewMov(direction, thisOne);
// check if the last movement is available
checkState(thisOne, direction);
// done
isTumbling = false;
}
```
Also there are a lot of constraints in the game, especially about colors and position/rotation. This part was really complicated - and I struggled hard against some non-understandable bugs. Finally I have this code which seems to me relevant and clever :
```
// check new position and rotation
private void checkState(GameObject thisOne, Vector3 direction)
{ // 1° check if a new step is available
if (!loader.GetComponent().giveMeNumber(loader.GetComponent().pattern, listMov.Count))
{
computeMove(-direction, thisOne, true, true);
// play a sound
if (loader.noMoreStep != null && loader != null)
loader.playSound(loader.noMoreStep, this.transform, true, 0.4f, 0.95f, _loader.noMoreStep.length, false);
// get out of there
return;
}
// 2° check cube face
if (listPos.Contains(thisOne.transform.position + new Vector3(1, 0, 0)) ||
listPos.Contains(thisOne.transform.position - new Vector3(1, 0, 0)) ||
listPos.Contains(thisOne.transform.position + new Vector3(0, 0, 1)) ||
listPos.Contains(thisOne.transform.position - new Vector3(0, 0, 1)))
{
foreach (GameObject g in cc)
{ // excludes cube player which are not in our radius
if (g != thisOne && Vector3.Distance(g.transform.position, thisOne.transform.position) < 1.1f)
{
var d = (thisOne.transform.position - g.transform.position).normalized;
// check if cube touching have the same color on their face
var isXA = Mathf.Abs(Vector3.Dot(d, thisOne.transform.right)) > 0.999f;
var isYA = Mathf.Abs(Vector3.Dot(d, thisOne.transform.up)) > 0.999f;
var isZ_A = Mathf.Abs(Vector3.Dot(d, thisOne.transform.forward)) > 0.999f;
var isX_B = Mathf.Abs(Vector3.Dot(d, g.transform.right)) > 0.999f;
var isY_B = Mathf.Abs(Vector3.Dot(d, g.transform.up)) > 0.999f;
var isZ_B = Mathf.Abs(Vector3.Dot(d, g.transform.forward)) > 0.999f;
bool sameColor = isX_A == isX_B && isY_A == isY_B && isZ_A == isZ_B;
// this movement is finally forbidden
if (!sameColor)
{
computeMove(-direction, thisOne, true, true); // back step
// get out of there
return;
}
}
}
}
// 3° check colored tiles
if ((Mathf.RoundToInt(thisOne.transform.eulerAngles.x) == 0 && Mathf.RoundToInt(thisOne.transform.eulerAngles.z) == 180) ||
(Mathf.RoundToInt(thisOne.transform.eulerAngles.x) == 0 && Mathf.RoundToInt(thisOne.transform.eulerAngles.z) == 0))
{ // red tile on ground
if (greenTilesPos.Contains(storedPos) || blueTilesPos.Contains(storedPos) || greenGoal.Contains(storedPos) || blueGoal.Contains(storedPos))
computeMove(-direction, thisOne, true, true);
else if (listBlank.Contains(storedPos))
blankTileProcess(1, storedPos);
else if (redGoal.Contains(storedPos))
levelSolved(thisOne);
// get out of there
return;
}
else if ((Mathf.RoundToInt(thisOne.transform.eulerAngles.x) == 0 && Mathf.RoundToInt(thisOne.transform.eulerAngles.z) == 270) ||
(Mathf.RoundToInt(thisOne.transform.eulerAngles.x) == 0 && Mathf.RoundToInt(thisOne.transform.eulerAngles.z) == 90))
{ // green tile on ground
if (redTilesPos.Contains(storedPos) || blueTilesPos.Contains(storedPos) || redGoal.Contains(storedPos) || blueGoal.Contains(storedPos))
computeMove(-direction, thisOne, true, true);
else if (listBlank.Contains(storedPos))
blankTileProcess(2, storedPos);
else if (greenGoal.Contains(storedPos))
levelSolved(thisOne);
// get out of there
return;
}
else if ((Mathf.RoundToInt(thisOne.transform.eulerAngles.x) == 270 && Mathf.RoundToInt(thisOne.transform.eulerAngles.z) == 0) ||
(Mathf.RoundToInt(thisOne.transform.eulerAngles.x) == 90 && Mathf.RoundToInt(thisOne.transform.eulerAngles.z) == 0))
{ // blue tile on ground
if (redTilesPos.Contains(storedPos) || greenTilesPos.Contains(storedPos) || redGoal.Contains(storedPos) || greenGoal.Contains(storedPos))
computeMove(-direction, thisOne, true, true);
else if (listBlank.Contains(storedPos))
blankTileProcess(3, storedPos);
else if (blueGoal.Contains(storedPos))
levelSolved(thisOne);
// get out of there
return;
}
}
// transform blank tile
void blankTileProcess(int color, Vector3 vPos)
{
int i = listBlank.IndexOf(vPos);
// UnityEngine.Debug.Log(listBlank.IndexOf(vPos) + " -> " + vPos);
// colored blank tile with a new color
if (Time.timeScale < 1.5f)
{
if (stampBlank[i] == 0)
{ // is this tile blank?
stampBlank[i] = listMov.Count;
obj.GetComponent().blankTiles.transform.GetChild(i).GetComponent().sharedMaterial = obj.GetComponent().materials[color];
// play a sound
if (loader.GetComponent().paint != null && loader != null)
loader.playSound(loader.paint, this.transform, true, 0.5f, 0.9f, _loader.paint.length, false);
// ... or you can use item selection instead of an integer
if (color == 1)
redTilesPos.Add(vPos);
else if (color == 2)
greenTilesPos.Add(vPos);
else if (color == 3)
blueTilesPos.Add(vPos);
else
return;
}
}
}
```
To get an inverted clockwise system, allowing the player to come back in time, I used a movement list according to the object and its direction. So the number of steps which are available is computed according to the List.Count integer. We got something like that :
```
// all movement have been saved so as to allow back in time
void backInTime()
{
while (listMov.Last().artefact != cc[index])
splitCube();
if (listBlank.Contains(cc[index].transform.position - shift))
{
int i = listBlank.IndexOf(cc[index].transform.position - shift);
// disable a blank tile which has been colored before
if (stampBlank[i] == listMov.Count)
{
obj.GetComponent<LevelConstructor>().blankTiles.transform.GetChild(i).GetComponent<Renderer>().sharedMaterial = obj.GetComponent<LevelConstructor>()._materials[7];
stampBlank[i] = 0;
// new blank status
redTilesPos.Remove(cc[index].transform.position - shift);
greenTilesPos.Remove(cc[index].transform.position - shift);
blueTilesPos.Remove(cc[index].transform.position - shift);
}
}
// invert movement
computeMove(-listMov.Last().movement, listMov.Last().artefact, false, true);
deleteLast();
}
```
My favorite feature is the rounded blank tile which the player paints when a cube walks on it. This is not an easy feature because of one point - you can back in time. So its status must change too. So I implemented a tag on them in order to "clean up" tiles when retrograded cube passes again on a previous tile which has been colored. This tag is just an integer - the step when the tile has been colored. Take a look at the end of the first script - the blankTileProcess() function. In the previous script, you got the code which compute the state of the tile during the right moment.
Some parts of these codes could be improved. As an a single example, I use Time.timeScale like a tag - and I don't like it. There are too many GetComponent occurences in these scripts - and it's not the best way to get a hand on an object. So I have many to do :wink:
For the FX I used some shaders and this project is my first one using the Unity URP system. I tested this option, but I guess that a legacy system could be "enough". I am developing more levels in order to release a full game with 50 levels. I have only one scene for them, and when you see a black screen, it's just a short time to erase the previous level, creating the next... Also I am proud of my level selection system. before I created a button for each level. Now I have only one script to generate how many buttons are required. This script is really useful. Then I share it with you.
```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DisplayButtons : MonoBehaviour
{
public Transform button;
public int buttonOnX = 10;
public int buttonOnY = 5;
Vector3 nextPos = Vector3.zero;
public int spacingX = 80;
public int spacingY = 80;
Vector3 shift;
int id = 0;
void Start()
{
displayThem();
}
public void displayThem()
{
nextPos.y = 0;
id = 0;
shift = new Vector3(((buttonOnX * spacingX)/2) - (spacingX/2), (-(spacingY * buttonOnY)/2) - (spacingY/2), 0);
for (int i = 0; i < buttonOnY; i++)
{
nextPos.y -= spacingY;
nextPos.x = 0;
for (int j = 0; j < buttonOnX; j++)
{
id++;
RectTransform myButton = Instantiate(button as RectTransform);
myButton.GetComponentInChildren<Text>().text = (id).ToString();
myButton.GetComponentInChildren<Text>().fontSize = 40;
myButton.GetComponentInChildren<Text>().color = new Color(0.2f, 0.22f, 0.5f, 0.9f);
myButton.GetComponentInChildren<Image>().color = new Color(0.7f, 0.5f, 0.8f, 0.9f);
myButton.SetParent(transform, false);
myButton.localPosition = nextPos - shift;
nextPos.x += spacingX;
int level = PlayerPrefs.GetInt("Level", 1);
if (id > level && !Application.isEditor)
{
myButton.GetComponentInChildren<Button>().interactable = false; // disables button for scene that isn't available
myButton.GetComponentInChildren<Text>().color = new Color(0.2f, 0.22f, 0.5f, 0.45f);
}
}
}
}
}
```
I exposed some parts of my project, just sharing with you some keys. I guess that it could be improved - and with more time, I will do it. If you want to test the result, the game is available on Itch.io for free. Play, leave a comment - and this way, I can find your own project which I wanna play. Thank you for your support everyone. I wish you the best ++
https://ldjam.com/events/ludum-dare/55/virtual-church-of-the-digital-goddess
