With the day loop now done, to a first pass, the night time awaits!
Well the start of the New Year was supposed to be an opportunity to really get stuck in with the development of the game. However, the realisms of life hit hard and work managed to level up quite quickly demanding a lot more of my time. This made initial progress of the night loop very slow, and put me quite a way behind the plan, but it is now slowly gathering speed with sectiosn being completed (to a first pass) at quite a rate.
Alongside Anouk has managed to get the customisation aspects of the game done to a first pass so we are beginning to see the fruits of our labour finally begin to show in the manner we were hoping.
The Game
So with the day loop complete we have managed to get some eyes on the game and get some valuable feedback. I managed to perform a play-test with a colleague who has given us some excellent feedback and pointed out some good areas for improvement and tweaking.

We conducted the team through Teams which enabled us to be able to record the participant playing the game while conversing with them to get immeadiate feedback to the interaction. Additionally it enabled us to really highlight how any bugs that were encountered we performed so that we would be able to track these down easily. It was quite apparent we have some scaling issues in the day loop with lots of areas to escape the environment, this will be easily recitified in a second pass however.
As always the latest version of the game is available on out itch.io page.
The Night
So far I have managed to get two section of the night loop completed, Forest Swoop and Castle Battle. Both of these required different controllers which , although twice the work, did not take very long to create. The real big stumbling blocks were getting the AI aspects to work within the environment.
Forest Swoop
The premise behind this section of the night loop is to basically become the health/energy recovery section. The previous two sections of the game play will use both health and energy as a way of timing the game play, with reduction of these two resources being the way you lose life. In Forest Swoop it is up to the player to replenish their health and energy to full in the quickest time possible to allow more time in later sections.
It is a top down viewpoint as you fly around as a dragon trying to swoop down on unsuspecting animals to feed upon and therefore replenish yourself. So the starting point had to be the top down view and controller.

The starting point was setting up the object in game. Obviusly it needed the Dragon model itself, along with a couple of Cinemachine cameras. There is also a couple of other elements required for the whole controller to work.
To get the basic locomotion working there was a little bit of a sticking point. Originally I tried to use a Character Controller component in Unity to make life a little easier when it come to collisions. This is where the first issue arose, as within this section of the game the dragon has to move forward at a constant speed without player input, and trying to get that working with the character controller alongside accepting input from the player for turning proved to be troublesome. The answer was to revert back to a Rigidbody based controller, and the second I did this everything just began to fall in to place.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void FixedUpdate() { rb.velocity = Vector3.zero; if (!killing){ rb.velocity = transform.forward * dragonSpeed * Time.deltaTime; if (Input.GetKey(KeyCode.A)) { transform.RotateAround(transform.position, -Vector3.up, rotateSpeed * Time.deltaTime); } else if (Input.GetKey(KeyCode.D)) { transform.RotateAround(transform.position, Vector3.up, rotateSpeed * Time.deltaTime); } if (canEagle) { mainCamBrain.m_DefaultBlend.m_Time = 0.3f; TopDownCam.m_Priority = 0; EagleEyeCam.m_Priority = 10; } else { TopDownCam.m_Priority = 10; EagleEyeCam.m_Priority = 0; } } } |
I was able to directly affect the rigidbody to move it forward while alongside use the A and D keys to turn the dragon. This gave us the basic movement controls and with a little tweaking of some variables to change speed and rotational speed I managed to get the dragon moving in a way that felt satisfying. Of course when I put it into the world that was when I worked out it would be an idea to turn the gravity off so that it did not just fall straight to the ground.
The next thing to attack was the Eagle Eye mechanic, where the player is able to zoom in towards the ground to get a better view. This was where the two cameras came into play with one have a smaller field of view, giving the illusion of being zoomed in without having to physically move the camera (the reason for two cameras instead of one with a FOV change was just incase this was not enough).
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
EagleActive = controls.Dragon.RightMouse.ReadValue<float>() > 0; if (EagleActive && eagleAmount> 0) { DecreaseEagleAmount(); canEagle = true; } else if (EagleActive && eagleAmount == 0) { canEagle = false; } else if (!EagleActive && eagleAmount > 0) { IncreaseEagleAmount(); canEagle = false; } else { IncreaseEagleAmount(); } uXManager.SetEagleEyeAmount(eagleAmount); |
To power the Eagle Eye mechanic we are using a basic UI bar that degrades as you use it and refills when it is not being used. It is activated on the Right Click of the mouse so I was able to check whether this was being clicked in order to reduce the amount and alternatively refill if it was not.
The last thing was to activate the actual swoop or attack. This was a simple case of just casting a ray from the centre of the camera into the game world and establishing what it was hitting. If it hit an enemy then attack, it it hit anything else then it should be considered a miss. Of course once fully implemented a lot more things would happen as it would need to communicate with the element it had hit and tell it to do things.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using Cinemachine; using DG.Tweening; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; [RequireComponent(typeof(Rigidbody))] public class DragonController : MonoBehaviour { private Rigidbody rb; private InputManager inputManager; private GameManager gameManager; private UXManager uXManager; private Vector3 dragonVelocity; private bool groundedDragon; private float currentVelocity; private PlayerControls controls; [Header ("Cameras")] public CinemachineVirtualCamera TopDownCam; public CinemachineVirtualCamera EagleEyeCam; private CinemachineVirtualCamera tempKillCam; private CinemachineBrain mainCamBrain; [Header ("Forest Swoop Stuff")] private bool EagleActive; private bool canEagle; private bool killing; private GameObject tempPig; private GameObject tempDragonModel; private GameObject tempPigModel; private GameObject tempEndSpot; private bool hpFlip; private bool loadingVillageAttack; private bool stormDamageActive; public Volume volume; Vignette vignette; PaniniProjection paniniProjection; bool takingDamage; public float stormDelay; public Canvas StormWarning; [SerializeField] private float dragonSpeed = 2.0f; [SerializeField] private float rotateSpeed = 2.0f; [SerializeField] private float eagleBlendTime = 0.3f; [SerializeField] private float eagleAmount = 1f; [SerializeField] private CanvasGroup aimReticle; [Header ("Animation stuff")] public Animator anim; public float flapFrequency = 0.01f; private float timeSinceFlap = 0f; private float tiltMin = -1f; private float tiltMax = 1f; private float tiltIncrement = 0.1f; private float tilt = 0f; private Animator tempAnim; private void Awake() { controls = new PlayerControls(); anim.ResetTrigger("FlapWings"); } private void OnEnable() { controls.Enable(); } private void OnDisable() { controls.Disable(); } // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody>(); inputManager = InputManager.Instance; gameManager = GameObject.Find("GameManager").GetComponent<GameManager>(); uXManager = GameObject.Find("GameManager").GetComponent<UXManager>(); mainCamBrain = Camera.main.GetComponent<CinemachineBrain>(); volume.profile.TryGet<Vignette>(out vignette); volume.profile.TryGet<PaniniProjection>(out paniniProjection); //Grabs the variables from the Game Manager dragonSpeed = gameManager.dragonSpeed; rotateSpeed = gameManager.dragonRotateSpeed; // Set anim correctly anim.SetFloat("Tilt", tilt); } private void Update() { if (!killing){ EagleActive = controls.Dragon.RightMouse.ReadValue<float>() > 0; if (EagleActive && eagleAmount> 0) { DecreaseEagleAmount(); canEagle = true; } else if (EagleActive && eagleAmount == 0) { canEagle = false; } else if (!EagleActive && eagleAmount > 0) { IncreaseEagleAmount(); canEagle = false; } else { IncreaseEagleAmount(); } uXManager.SetEagleEyeAmount(eagleAmount); if (inputManager.DragonLeftClickThisFrame()) { tilt = 0f; anim.SetFloat("Tilt", tilt); anim.SetTrigger("Swoop"); RaycastHit hit; Ray ray = Camera.main.ScreenPointToRay(Camera.main.transform.position); if (Physics.Raycast (Camera.main.transform.position, Camera.main.transform.forward * 100f, out hit)) { Debug.DrawRay (Camera.main.transform.position, Camera.main.transform.forward * 100f, Color.red); if (hit.collider.gameObject.tag == "forestHit") { hit.collider.gameObject.transform.parent.gameObject.GetComponent<ForestSwoopAI>().caught = true; hit.collider.gameObject.transform.parent.gameObject.GetComponent<Rigidbody>().isKinematic = true; //grab the stuff I need tempPig = hit.collider.gameObject.transform.parent.gameObject; tempPig.GetComponent<ForestSwoopAI>().SetAnim(1); tempKillCam = tempPig.GetComponent<ForestSwoopAI>().KillCam; tempDragonModel = tempPig.GetComponent<ForestSwoopAI>().dragonModel; tempPigModel = tempPig.GetComponent<ForestSwoopAI>().pigModel; tempEndSpot = tempPig.GetComponent<ForestSwoopAI>().endSpot; tempAnim = tempDragonModel.GetComponent<Animator>(); StartCoroutine (KillAnimate()); } else { Debug.Log ("MISSED"); } } } else { anim.ResetTrigger("Swoop"); } } if (stormDamageActive) { vignette.intensity.value = Mathf.PingPong (Time.time * 2, 0.5f); //paniniProjection.distance.value = Mathf.PingPong (Time.time * 2, 1); StormWarning.enabled = true; if (!takingDamage){ StartCoroutine(TakeStormDamage()); } } else { vignette.intensity.value = Mathf.MoveTowards (vignette.intensity.value , 0f, 0.2f * Time.deltaTime); //paniniProjection.distance.value = Mathf.MoveTowards (paniniProjection.distance.value , 0.1f, 0.2f * Time.deltaTime); StormWarning.enabled = false; } if (gameManager.HealthAmount == 0) { //DO SOME END GAME STUFF } } // Update is called once per frame void FixedUpdate() { rb.velocity = Vector3.zero; if (!killing){ rb.velocity = transform.forward * dragonSpeed * Time.deltaTime; if (Input.GetKey(KeyCode.A)) { if (tilt > tiltMin){ tilt -= tiltIncrement; } transform.RotateAround(transform.position, -Vector3.up, rotateSpeed * Time.deltaTime); } else if (Input.GetKey(KeyCode.D)) { if (tilt < tiltMax){ tilt += tiltIncrement; } transform.RotateAround(transform.position, Vector3.up, rotateSpeed * Time.deltaTime); } else { // No input means we can start tilting back to base if (tilt > 0.01f) { tilt -= tiltIncrement; } else if (tilt < -0.01f) { tilt += tiltIncrement; } else { tilt = 0f; } } anim.SetFloat("Tilt", tilt); // Flap wings occasionally timeSinceFlap += 1f; if (timeSinceFlap >= 1f/flapFrequency) { anim.SetTrigger("FlapWings"); timeSinceFlap = 0f; Debug.Log("Flapping"); } if (canEagle) { mainCamBrain.m_DefaultBlend.m_Time = 0.3f; TopDownCam.m_Priority = 0; EagleEyeCam.m_Priority = 10; } else { TopDownCam.m_Priority = 10; EagleEyeCam.m_Priority = 0; } } if (gameManager.HealthAmount == 1f && gameManager.EnergyAmount == 1f) { if (!loadingVillageAttack){ loadingVillageAttack = true; uXManager.DragonGroupFade(0f); uXManager.LoadScene(7); } } } IEnumerator KillAnimate() { killing = true; uXManager.DragonGroupFade(0f); aimReticle.alpha = 0f; mainCamBrain.m_DefaultBlend.m_Time = 0; tempKillCam.m_Priority = 40; yield return new WaitForSeconds (1f); tempDragonModel.SetActive(true); tempAnim.SetTrigger("Swoop"); tempDragonModel.transform.DOMove (tempPig.transform.position, 1f); Vector3 tempPos = new Vector3 (0, 0, tempPig.transform.position.z + 1f); yield return new WaitForSeconds (1.5f); tempPigModel.transform.parent = tempDragonModel.transform; tempDragonModel.transform.DOMove (tempEndSpot.transform.position, 2f); yield return new WaitForSeconds (1.5f); Destroy(tempPig); mainCamBrain.m_DefaultBlend.m_Time = 1; tempKillCam.m_Priority = 0; if (hpFlip){ gameManager.PlusHealth(gameManager.ForestSwoopReplenish); hpFlip = false; } else { gameManager.PlusEnergy(gameManager.ForestSwoopReplenish); hpFlip = true; } uXManager.DragonGroupFade(1f); aimReticle.alpha = 1f; killing = false; } private void DecreaseEagleAmount(){ eagleAmount = eagleAmount -= Time.deltaTime; eagleAmount = Mathf.Clamp(eagleAmount, 0f, 1f); } private void IncreaseEagleAmount(){ eagleAmount = eagleAmount += Time.deltaTime; eagleAmount = Mathf.Clamp(eagleAmount, 0f, 1f); } private IEnumerator BrainReset() { yield return new WaitForSeconds(eagleBlendTime); mainCamBrain.m_DefaultBlend.m_Time = 1f; } private void OnTriggerExit(Collider other) { if (other.gameObject.tag == "forestStorm") { stormDamageActive = true; } } private void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "forestStorm") { stormDamageActive = false; } } IEnumerator TakeStormDamage(){ takingDamage = true; gameManager.MinusHealth(gameManager.ForestSwoopReplenish); yield return new WaitForSeconds (stormDelay); takingDamage = false; } } |
Over the course of the development of this section the Dragon Controller grew as it needed to interact with the AI of the enemies as well as talk to the Forest Swoop controller that manages this section of the game and handle the animation from Anouk. As you can see from the script above it did begin to become quite a large script.
The Forest Swoop Enemies
What was required for this was an AI object that would seeminlgy look to be just roaming around the map. Firstly I knew I would have to use Unity’s NavMesh functionality but having used this before I know there are some excellent tools that add to it from Unity themselves along with some useful documentation. I added the navigation tools package and firstly added a NavMesh Surface to the game world, of course in doing this I found out very quickly the the daytime environment we were planning on using for this was way too big. So I quickly constructed a new one that could be used just for the Forest Swoop section.

The next job was to spawn the enemies on to the environment so I did some digging in to the documentation to see if there was a way to randomly find a position on the NavMesh in order to spawn enemies on to. While there does seem to be a way to do this after a few attempts I could not get the enemies to spawn in the area I wanted them to, so I went back to some old code from a previous project that I had used before to spawn trees into a prescribed area. The way this works it to define an area in the game using floats for x and y, and then to randomly chose a position within the area to be our spawnpoint check. We then cast a ray straight down and see if we are hitting the NavMesh, if we are the point is valid and its spawns an enemy, if it is not it moves on to another random position and checks again. Once it has done this enough time to fill the enemy amount variable it stops.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private void SpawnEnemies(){ randomPosition = (transform.localPosition + centre) + new Vector3 (Random.Range(-size.x / 2, size.x / 2), Random.Range(-size.y / 2, size.y / 2), Random.Range(-size.z / 2, size.z / 2)); RaycastHit hit; Ray ray = new Ray (randomPosition, Vector3.down); Physics.Raycast (ray, out hit); if (hit.collider.tag =="forestGround") { spawnPosition = hit.point; } else { return; } GameObject clone = Instantiate (enemyPrefab, spawnPosition, Quaternion.identity); currentNumberEnemies++; } |
So now all you need to do is set the amount of enemies you want it to spawn and it will cycle through the code until all enemies are on the map in the area you want them to be.
Now all that was left was for the AI to move around as if roaming. So in its basic term I needed the AI to move to a point and once arriving at this point, pick another point to then roam to. In order for this to be believable though the new destination needed to be somewhat near to where the AI currently is, otherwise it would pick a destination that would take too long to travel to and you would just have AI moving around in straight lines.
So what I did was use a radius around the enemy as a boundary and pick a random spot within this radius, this then meant the AI would in theory roam around within an area close to its original position but not too far to think it was unbelievable. Then i just had to fire this choice whenever it reached its destination.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void Update() { if (!caught){ //Check if arrived at destination if (!agent.pathPending) { if (agent.remainingDistance <= agent.stoppingDistance) { if (!agent.hasPath || agent.velocity.sqrMagnitude == 0f) { FindNewDestination(); } } } agent.SetDestination (currentDestination); } else { //stop it when its caught agent.isStopped = true; } } void FindNewDestination() { Vector3 randomDirection = Random.insideUnitSphere * moveRadius; randomDirection += transform.position; NavMeshHit rayHit; NavMesh.SamplePosition (randomDirection, out rayHit, moveRadius, 1); Vector3 newDestination = rayHit.position; currentDestination = newDestination; } |
Once this was all working, and checks where made to see if you had managed to hit an enemy I add in a small cutscene to play of the dragon swooping down on the enemy and taking it away. This gave the player some immeadiate feedback that they had been successful, and I was able to add the health/energy recovered and this was Forest Swoop basically done to a first pass.
Castle Battle
This section was relatively easy to build thanks largely to the fact that at its core it is just Space Invaders. Enemies run into the world and start firing arrows at you, you have to shoot them before they get to you. If they hit you your health depletes and when it gets down to 25% the game ends. There is also a small smart bomb mechnic which involves oil barrels, so yes its Space Invaders.

Anouk had already created all of the assets for this section of the game so construction was pretty simple. As there was no real moving aspect of the dragon it was just a case of creating a mouse point driven aiming system. Thankfully this is pretty much built in to Unity.
|
1 |
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); |
The above line of code simply find where the mouse pointer is on the screen and projects a ray down in to the world giving you the position in 3D space. From that I was able to establish where I need to fire the VFX that Anoul had provided and the fireball was working. I was also able to create a target in the game using a canvas that follows this position, giving the player an instant understanding of where they are aiming.
The next aspect was to add in the enemies. I tried not to overly complicate this so just used some basic destination setting through points in the world. Obviously within the script below there is also the shooting of arrows and the death mechanics for when you successfully hit one with a fireball.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class CastleAttackAI : MonoBehaviour { private NavMeshAgent agent; public Transform destination; private Transform dragonPos; private float rotationSpeed = 10f; public CastleBattleEnemyManager enemyManager; public GameObject firePoint; public GameObject projectile; public float fireDelay; private GameObject Trashcan; bool Shootable; bool canShoot; public bool Dead; [Header ("Animation stuff")] private Animator anim; // Start is called before the first frame update void Start() { dragonPos = GameObject.Find("dragonPerched").transform; agent = GetComponent<NavMeshAgent>(); agent.SetDestination (destination.position); Trashcan = GameObject.Find("Trashcan"); anim = GetComponentInChildren<Animator>(); anim.SetBool("IsSprinting", true); } // Update is called once per frame void Update() { //ANIMATION////////////////////////////////////// //Needs to be walking/running by default ///////////////////////////////////////////////// //Walking if (!agent.pathPending) { if (agent.remainingDistance <= agent.stoppingDistance) { //ANIMATION////////////////////////////////////// //Need to stop walking and stand in idle. ///////////////////////////////////////////////// anim.SetBool("IsSprinting", false); RotateTowards(dragonPos); if (!agent.hasPath || agent.velocity.sqrMagnitude == 0f) { agent.isStopped = true; Shootable = true; } } } //Dieing if (Dead) { StartCoroutine(Death()); } //Shooting if (Shootable && canShoot == false) { StartCoroutine(ShootArrow()); } } private void RotateTowards (Transform target) { Vector3 direction = (target.position - transform.position).normalized; Quaternion lookRotation = Quaternion.LookRotation(direction); transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * rotationSpeed); } IEnumerator Death() { Dead = false; enemyManager.hasEnemy = false; //ANIMATION////////////////////////////////////// //Play death anim. Alter the delay below to the length. ///////////////////////////////////////////////// anim.SetTrigger("Dead"); yield return new WaitForSeconds (1.6f); Destroy(gameObject); } IEnumerator ShootArrow(){ canShoot = true; yield return new WaitForSeconds (Random.Range (2f, 6f)); GameObject arrow = Instantiate (projectile, firePoint.transform.position, Quaternion.identity); arrow.transform.parent = Trashcan.transform; //ANIMATION////////////////////////////////////// //Can play the fire arrow animation here. Alter the fireDelay to be the length of the animation. ///////////////////////////////////////////////// anim.SetBool("IsShooting", true); yield return new WaitForSeconds (fireDelay); anim.SetBool("IsShooting", false); canShoot = false; } } |
So with the basic AI and a subsequent Enemy Manager script, used to control how quickly enemies respawn, the basic mechanics of the game were done. This was probably the easiest sections of the game to create. It did help that most of the animations were also complete as Anouk was way ahead of me with the workload.
I also managed to come up with a small bit of narrative design to place the oil barrel (smart bomb) in to the world by reusing the animal assets from Forest Swoop. So now a little pig with a a little cart brings in the oil barrel from off screen, deposits it in the world then leaves again. Once you blow it up another one soon comes along.
Next Steps
The next thing to tackle is the Animal Chase section, which involves the 3d flying of the dragon. I have already decided that this will be done through a Rigidbody controller to just make things easier but it is something that it going to take a lot of thought due to the way we need it to work and be controlled.
References
UNITY (no date) Navigation and pathfinding: Ai navigation: 1.1.1, AI Navigation | 1.1.1. Available at: https://docs.unity3d.com/Packages/com.unity.ai.navigation@1.1/manual/index.html (Accessed: February 26, 2023).
Figures
Featured Image: Charlish, R (2022) ‘Draak & Draig’ Created on 12th October 2022
Figure 1: Charlish, R (2023) ‘Playtest of the game conducted through Teams to be able to see how a player interacts.’ Created on 26th February 2023
Figure 2: Charlish, R (2023) ‘The Dragon prefab and controller for Forest Swoop along with game play view.’ Created on 26th February 2023
Figure 3: Charlish, R (2023) ‘Forest Swoop environment made specifically for this section of the game’ Created on 26th February 2023
Figure 4: Charlish, R (2023) ‘The Castle Battle game area and view’ Created on 26th February 2023
0 Comments