Unity3D 2018.2
Problem: Grid not being populated by list, not responding correctly to
the List<> being filled with Transforms, contains at least 1 item in List<> so it should not be empty. Something is not being transferred right
I'm trying to create a Grid Layout in a scroll view, which is filled with buttons containing transforms kept in a List<>
I get the transforms from checking a GameObject which will usually have 0-25 child transforms in it.
Once it gets all the child transforms from the parent's GameObject, check which child has a tag called "Satellite". After fill the Grid with the List<> containing those certain gameObject.transforms.
Clicking the buttons in the grid should contain the transform, for example OnMouseEnter() in the script if I use Debug.Log(transform.name) it should display it.
Here is the code I'm using which contain no errors, the grid is empty so I'm not receiving the transforms correctly but I don't know what is wrong with the code. Thank you for the help.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SatelliteGridControl : MonoBehaviour {
private List<Transform> satelliteListFromPlanet;
[SerializeField]
private GameObject buttonTemplate;
[SerializeField]
private GridLayoutGroup gridGroup;
[SerializeField]
private Sprite[] iconSprites;
// Use this for initialization
void OnEnable()
{
getSatellitesInPlanet();
satelliteListFromPlanet = new List<Transform>();
for (int i = 1; i <= satelliteListFromPlanet.Count; i++)
{
SatTransfrom newSatellite = new SatTransfrom();
newSatellite.iconSprite = iconSprites[Random.Range(0, iconSprites.Length)];
satelliteListFromPlanet.Add(newSatellite);
}
GenInventory();
}
// Get Satellites
private void getSatellitesInPlanet()
{
satelliteListFromPlanet = new List<Transform>();
// Get current planet
Transform currentPlanet = GameObject.FindGameObjectWithTag("MainCamera").GetComponent<HandleCamera>().targetToLookAt;
// Check inside for satellites
foreach (Transform satellite in currentPlanet)
{
// Check transform for tag
if (satellite.CompareTag("Satellite"))
{
// Add each transform from planet to array
satelliteListFromPlanet.Add(satellite);
}
}
}
// Handle Grid
private void GenInventory()
{
if (satelliteListFromPlanet.Count < 6)
{
gridGroup.constraintCount = satelliteListFromPlanet.Count;
}
else
{
gridGroup.constraintCount = 5;
}
foreach (SatTransfrom sat in satelliteListFromPlanet)
{
GameObject newButton = Instantiate(buttonTemplate) as GameObject;
newButton.SetActive(true);
newButton.GetComponent<SatelliteButton>().SetIcon(sat.iconSprite);
newButton.transform.SetParent(buttonTemplate.transform.parent, false);
}
}
public class SatTransfrom : Transform
{
public Sprite iconSprite;
}
}
In OnEnable you first call getSatellitesInPlanet to populate your list satelliteListFromPlanet.
But right after you finished you call
satelliteListFromPlanet = new List<Transform>();
Which resets your list to a new empty one.
Than you have a loop
for (int i = 1; i <= satelliteListFromPlanet.Count; i++)
{ //... }
But since satelliteListFromPlanet is an empty list at this moment nothing happens.
And finally when you call GetInventory your list is still empty so
foreach (SatTransfrom sat in satelliteListFromPlanet)
Is executed never since there are no elements in satelliteListFromPlanet.
Now to the second problem:
You have
for(int i = 0; i< sateliteLostFromPlanet.Count; i++)
But inside of this loop you do
sateliteListFromPlanet.Add(xy);
... So what happens to your List during running this loop?
It grows bigger 1 elememt each loop so your loop condition i < sateliteListFromPlanet.Count will allways be true since after every execution your list is 1 element longer and i is 1 bigger!
Result: You add more and more elements to the same list "forever" until your device runs out of memory.
Related
I am trying to set up a system that displays the ground loot near the player (i.e., actors that are overlapping the player's UBoxComponent). This needs to change as and when the player moves and the ground loot actors are no longer being overlapped.
I have sort of got it working - when the actors are on their own the function seems to work. However, when two ground loot actors are close to one another, the inventory displays multiple widgets for the same overlapping actor on the floor and sometimes only recognises one of the actors on the floor. It seems to be having trouble when the Player's UBoxComponent overlaps more than one actor at a time. I'll link a YouTube video to help show the issue I'm having because it's quite hard to describe: https://youtu.be/a_zMl1zOUDc
This is the function for updating the ground loot widgets. It is called by a dynamic multicast delegate that is broadcasted from the player class:
// Iterates through the ground loot around the player and displays a widget for each item
void UInventory::UpdateGroundLoot()
{
MyPlayer = Cast<AMainCharacter>(GetOwningPlayer()->GetCharacter());
if(!MyPlayer) { return; } // Null check
// Get a ref to the Player's inventory component
UInventoryComponent* PlayerInventoryComp = MyPlayer->PlayerInventory;
// Clear any current widgets in the ground loot list
GroundLootScrollBox->ClearChildren();
// GroundLootActorsArray is an array of AActors that gets added to/emptied based on AActors that overlap with the Player's UBoxComponent
if(!PlayerInventoryComp && !PlayerInventoryComp->GroundLootActorsArray.IsValidIndex(0)) { return; } // Null check
for(int32 i = 0; i < PlayerInventoryComp->GroundLootActorsArray.Num(); i++)
{
// Cast each element of the GroundLootItems array to AItem* GroundItem
AItem* GroundItem = Cast<AItem>(PlayerInventoryComp->GroundLootActorsArray[i]);
if(!GroundItem) { return; } // Null check
// Add GroundItem to a new array (GroundLootItemsArray). This is an array specifically of AItems (rather than AActors).
GroundLootItemsArray.Add(GroundItem);
GroundLootWidget = CreateWidget<UInventoryItemWidget>(GetOwningPlayer(), InventoryItemWidgetClass);
// Add each created widget to an array of widgets called GroundLootWidgetsArray
GroundLootWidgetsArray.Add(GroundLootWidget);
if(!GroundLootWidgetsArray.IsValidIndex(0) && !GroundLootItemsArray.IsValidIndex(0)) { return; } // Null check
// Display item specific names/thumbnails for each widget created
GroundLootWidgetsArray[i]->ItemName->SetText(GroundLootItemsArray[i]->ItemDisplayName);
GroundLootWidgetsArray[i]->Thumbnail->SetBrushFromTexture(GroundLootItemsArray[i]->Thumbnail);
// Add each array element of the GroundLootWidgetsArray to the scroll box in the widget blueprint
GroundLootScrollBox->AddChild(GroundLootWidgetsArray[i]);
}
}
DevilsD in the comments pointed me in the right direction here. There were two main issues I had.
The delegate that was designed to update the ground loot widgets was being broadcasted in a for loop for every element of the OverlappingActor array. I think this resulted in multiple widgets being displayed for the same actor. The code in the Player's class now looks like this (TriggerEnter and TriggerExit are called by the OnComponentBeginOverlap and OnComponentEndOverlap delegates in the constructor respectively) :
void AMainCharacter::TriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
bItemIsWithinRange = true;
// I think the TSubclassOf ensures that we only pickup AItem classes and it's children - rather than all actors on the ground
OverlappedComponent->GetOverlappingActors(OverlappingActors, TSubclassOf<AItem>(AItem::StaticClass()));
if(PlayerInventory && OverlappingActors.IsValidIndex(0))
{
for(auto& GroundItems : OverlappingActors)
{
PlayerInventory->AddItemToGroundLoot(GroundItems);
}
OnUpdateGroundLoot.Broadcast();
}
}
void AMainCharacter::TriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
bItemIsWithinRange = false;
if(PlayerInventory && OverlappingActors.IsValidIndex(0))
{
for(auto& GroundItems : OverlappingActors)
{
PlayerInventory->GroundLootActorsArray.Remove(GroundItems);
}
OnUpdateGroundLoot.Broadcast();
OverlappingActors.Empty();
}
}
I wasn't emptying the GroundLootItemsArray every time I was updating the ground loot and so previous ground loot was being stored in this array on each new update. This also resulted in showing the same widget for two different actors. I have now set it to empty along with clearing the children of the scroll box before any widget creation takes place in the UInventory.cpp (which is a child of UUserWidget):
// Iterates through the ground loot around the player and displays a widget for each item
void UInventory::UpdateGroundLoot()
{
MyPlayer = Cast<AMainCharacter>(GetOwningPlayer()->GetCharacter());
if(!MyPlayer) { return; } // Null check
// Get a ref to the Player's inventory component
UInventoryComponent* PlayerInventoryComp = MyPlayer->PlayerInventory;
// Clear any current widgets in the ground loot list
GroundLootScrollBox->ClearChildren();
// Empties this array to prevent replication of Item specific information that gets sent to the widget (i.e., the thumbnail or the name of the item)
GroundLootItemsArray.Empty();
// GroundLootActorsArray is an array of AActors that gets added to/emptied based on AActors that overlap with the Player's UBoxComponent
if(!PlayerInventoryComp && !PlayerInventoryComp->GroundLootActorsArray.IsValidIndex(0)) { return; } // Null check
for(int32 i = 0; i < PlayerInventoryComp->GroundLootActorsArray.Num(); i++)
{
// Cast each element of the GroundLootItems array to AItem* GroundItem
AItem* GroundItem = Cast<AItem>(PlayerInventoryComp->GroundLootActorsArray[i]);
if(!GroundItem) { return; } // Null check
// Add GroundItem to a new array (GroundLootItemsArray). This is an array specifically of AItems (rather than AActors).
GroundLootItemsArray.AddUnique(GroundItem);
GroundLootWidget = CreateWidget<UInventoryItemWidget>(GetOwningPlayer(), InventoryItemWidgetClass);
// Add each created widget to an array of widgets called GroundLootWidgetsArray
GroundLootWidgetsArray.AddUnique(GroundLootWidget);
if(!GroundLootWidgetsArray.IsValidIndex(0) && !GroundLootItemsArray.IsValidIndex(0)) { return; } // Null check
// Display item specific names/thumbnails for each widget created
GroundLootWidgetsArray[i]->ItemName->SetText(GroundLootItemsArray[i]->ItemDisplayName);
GroundLootWidgetsArray[i]->Thumbnail->SetBrushFromTexture(GroundLootItemsArray[i]->Thumbnail);
// Add each array element of the GroundLootWidgetsArray to the scroll box in the widget blueprint
GroundLootScrollBox->AddChild(GroundLootWidgetsArray[i]);
}
}
I also changed the adding of actors/items to the arrays from Array.Add(Actor) to Array.AddUnique(Actor). I'm not sure if this was necessary but it made sense to me because I didn't want more than one of any given item accidently being added to the array.
I tried this code to get the first visible row in a scrolling Table inside a BorderLayout.CENTER, but it didn't work, seems the points returned do not reflect the visible cells, unless I am missing a sort of calculation,
thank you for your insights,
#Override
protected void onScrollY(int scrollY) {
super.onScrollY(scrollY); //To change body of generated methods, choose Tools | Templates.
Component c=getComponentAt(50, scrollY);
if (c instanceof Table){
System.err.println("table "+getWidth()+" "+getHeight()+" s "+scrollY);
return;
}
Button b=(Button) c;
System.err.println("c: "+b.getText());
}
getComponentAt(x,y) takes absolute (screen) coordinates. The scrollY value is a relative coordinate in that container.
So what you want is something like:
Component c = getComponentAt(getAbsoluteX()+50, getAbsoluteY() + scrollY)
Also worth nothing that getComponentAt(x,y) will only return components that are focusable or have been set to grab pointer events. If you just want to find the first paintable immediate child of this container, and you're using a BoxLayout.Y_AXIS layout, then you might be better to just iterate through the children until you find one where y is at least the scrollY.
e.g.
Component c = null;
for (Component child : this) {
if (child.getY() + child.getHeight() > scrollY) {
c = child;
break;
}
}
....
Sorry if the code is sloppy, I've only been working with QT for the past couple of weeks. I'm working on a small game while studying, and right now I'm working on when the player buys an item from the shop it will be placed it into the character bag.
The Problem is when I buy the item once, it works fine. But if I delete the item after purchase then repurchase the same item again it will put two of the same item into the array even though I purchased one.
I have 3 files used for this. Inventory.cpp, GameScreen.cpp, Shops.cpp.
GameScreen.cpp is the main file. Here I just initialize everything.
void GameScreen::initGame(QString &characterName, QString &characterProfession){
//PASS CHARACTERNAME AND CHARACTERPROFESSION INTO INVENTORY
inv.initCharacter(characterName, characterProfession);
//INIT BAG
inv.initBag();
//INIT MONEY
inv.initMoney();
//SHOPS INITS
mos.initShop();
mos.passMoneyToShop(inv.gold, inv.silver, inv.copper);
}
This will open the shop window. The Item connect is apart of my problem.
void GameScreen::on_mapOneShopB_clicked()
{
mos.setModal(true);
//SEND THE ITEM INTO BAG
connect(&mos, SIGNAL(getItemFromMapOneShop(const QString&)), &inv, SLOT(bagAddElement(const QString&)));
mos.show();
mos.exec();
}
Moving into Shops.cpp. I use polymorphism in this file and that's the reason why you see MapOneShop:: instead of Shops::. Sorry about the confusion. But moving on, I hit the button and it subtracts the item price from my amount of gold.
void MapOneShop::on_buyB_clicked()
{
//ONLY WORKS IF I HIT THE ITEM NAME COLUMN THEN HIT BUY
gold -= itemPrice[ui->treeWidget->currentColumn()];
//UPDATE INFORMATION
updateInformationVAndMoneyAfterBuy();
}
It then moves into updateInformationVAndMoneyAfterBuy(); which checks that I spent money, and If I did it will emit the item I need. I just emit back to GameScreen.cpp inside of void GameScreen::on_mapOneShopB_clicked() and pass them into Inventory.cpp.
void MapOneShop::updateInformationVAndMoneyAfterBuy()
{
//UPDATE INFORMATIONV FIRST TO CHECK MONEY CHANGES
if(goldCheck != gold)
{
emit getUpdatedMoneyFromShop(gold, silver, copper);
//ITEM NAME IS WHERE I STORE THE NAMES OF THE ITEMS
emit getItemFromMapOneShop(this->itemName[ui->treeWidget->currentColumn()]);
}
}
Now moving into Inventory.cpp. When the item gets passed into bagAddElement after deleting then repurchasing, I get two of the same items even though only 1 should be passed in. I am using a dynamic array for this. Below I will also show the void Inventory::on_deleteB_clicked, void Inventory::bagDeleteAt and also void Inventory::bagLWPrint functions. The items value is my array holding the shopItem strings.
void Inventory::bagAddElement(const QString& shopItem)
{
//I USE THIS TO CHECK THE VALUE shopItem.
qDebug()<<"bagAddElement: " << shopItem;
//IF THE CURRENT POSITION IN THE BAG
//IS BIGGER THAN THE CURRENT SIZE
//IT WILL INCREASE THE BAG FOR US
if(nrOfEl >= bagSize)
{
bagExpand();
}
//CHECK FOR VALUES INSIDE ARRAY AND CHECK IF THEY ARE NULL
//IF SO, IT WILL ADD THE ITEM INTO THE NULL POSITION
for(int i = 0; i < bagSize; i++)
{
if(items[i] == nullptr)
{
nrOfEl = i;
items[nrOfEl++] = shopItem;
break;
}
}
//UPDATE WIDGET LIST
bagLWPrint();
}
Here I pass the currently selected row value inside bagLW and pass it into void Inventory::bagDeleteAt.
void Inventory::on_deleteB_clicked()
{
//BagLW IS A LIST WIDGET
bagDeleteAt(bagLW->currentRow());
}
Now we look for the what's inside items[row] and set it to nullptr. Then we go into void Inventory::bagLWPrint.
void Inventory::bagDeleteAt(int row)
{
if(items[row] == nullptr)
{
//IF THE ITEM IS ALREADY NULL WILL PRINT A MESSAGE
QMessageBox::information(this,"Bag","No item in that slot");
}
else
{
//SET CURRENTLY SELECTED ITEM TO NULL
items[row] = nullptr;
bagLWPrint();
}
}
I clear bagLW and refill it with the updated array.
void Inventory::bagLWPrint()
{
bagLW->clear();
for(int i = 0; i < bagSize; i++)
{
if(items[i] != nullptr)
{
bagLW->addItem(items[i]);
}
if(items[i] == nullptr)
{
bagLW->addItem(items[i]);
}
}
}
Other notes.
-The item only gets doubled when inserted into void Inventory::bagAddElement.
-I tried adding another item inside of void GameScreen::initGame and it did not double after deleting and re-entering.
Sorry for such a long question. And any help would be much, much appreciated.
void Inventory::bagExpand()
{
//1: INCREASE BAGSPACE
bagSize *= 2;
//2: CREATE TEMP ARRAY
QString *tempItems = new QString[bagSize];
//3: COPY OVER VALID VALUES FROM OLD ARRAY
for(int i = 0; i < nrOfEl; i++)
{
tempItems[i] = items[i];
}
//4: DELETE OLD ARRAY MEMORY
delete[] items;
//5: POINT OLD ARRAY POINTER TO NEW ARRAY LOCATION
items = tempItems;
//PRINT BAGLW - (UDPATE)
bagLWPrint();
qDebug()<<"Bag has increased";
}
The problem is that you connect and re-connect the getItemFromMapOneShop signal on every button click. That means that on first click, you add it once (signal triggered, 1 slot connected). On the second click, you add it twice (signal triggered, 2 slots connected). And so on.
QObjects manage a list of all connected slots per signal, and call each of them. Connecting a slot multiple times will call it that many times
You should connect the singal only once, e.g. in the constructor; or disconnect the signal when no longer used
As #Andéon Evain pointed out, you could also use Qt::UniqueConnection. This will not add a duplicate connection if it already exists (considering sender, signal, receiver, slot). That might be useful for cases where it's unknown if already connected; not in your simple case
I was trying to make a program that updated the amount of hearts the player has every turn (full, half and empty hearts). When I was doing this I instantiated a gameobject of that prefab as a variable and then assigned it to my UI panel in unity. However (I'm not sure but I think that) the variables used in the update just before are still being referenced after being destroyed in the next loop giving me the error:
MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
Here is the update loop:
void Update()
{
if (HP <= 0)
{
anim.SetBool("Death", true);
Destroy(GameObject.Find("Hearts"));
Destroy(GameObject.Find("Inventory"));
text.alignment = TextAnchor.LowerCenter;
text.text = "\n YOU DIED";
text.fontSize = 150;
text.color = new Color(255, 0, 0);
} else {
foreach (Transform child in GameObject.Find("Hearts").transform)
{
Destroy(child.gameObject);
}
for (var i = 0; i<(int)HP; i++)
{
GameObject Heart = Instantiate(heart, new Vector3(0, 0, 0), Quaternion.identity) as GameObject;
Heart.transform.SetParent(GameObject.Find("Hearts").transform);
}
if (HP - (float)((int)HP) == 0.5F) {
GameObject HalfHeart = Instantiate(halfheart, new Vector3(0, 0, 0), Quaternion.identity) as GameObject;
HalfHeart.transform.SetParent(GameObject.Find("Hearts").transform);
}
for (var i =0; i<Mathf.Floor(MaxHP-HP); i++)
{
GameObject EmptyHeart = Instantiate(emptyheart, new Vector3(0, 0, 0), Quaternion.identity) as GameObject;
EmptyHeart.transform.SetParent(GameObject.Find("Hearts").transform);
}
}
Is there a way to instantiate a prefab without making a variable?, or a way to make the variable reference temporary so it only lasts one update?
Thank you for your help in advance!
The problem is that once HP goes below zero, every subsequent update will enter the first if-statement and try to delete the "Hearts" and "Inventory" objects over and over. You can solve this by adding a bool called isDead and change the statement to if (HP <= 0 && !isDead) and then set isDead = true inside the block. This will prevent it from entering it twice.
Frankly though, your way of solving things is entirely backwards. As others have pointed out, deleting and instantiating objects every frame is very inefficient, and Transform.Find is also slow. You don't really need to destroy anything at all - you can rather just have a list of hearts and enable/disable an appropriate amount whenever the HP changes. You can have a single half-heart at the end of the list and enable/disable it when appropriate - if you are using a HorizontalLayoutGroup, it will still align correctly. You might want to make it so that you can only change the HP using a property or function (something like ModifyHealth(float amount)), and put the logic for updating the hearts display in there.
http://qt-project.org/doc/qt-4.8/qgraphicsscene.html#addItem
said
If the item is already in a different scene, it will first be removed from its old scene, and then added to this scene as a
top-level.
I want to keep the item in old scene.
how can I do this?
myscene1.addItem(item);
myscene2.addItem(item);// I don't want to remove item from myscene1
You can make a copy of the item :
myscene1.addItem(item);
myscene2.addItem(item->clone());
An item cannot occupy two scenes at the same time, in the way that you cannot be in two places at the same time.
The only way to do this is to make a copy of the item and place it in the second scene.
What you could do is to create a new class. e.g.
class Position
{
...
QPoinfF pos;
...
}
Then you can add that class to your item.
class Item : public QGraphicsItem
{
...
public:
void setSharedPos(Position *pos)
{
sharedPosition = pos;
}
//implement the paint(...) function
//its beeing called by the scene
void paint(...)
{
//set the shared position here
setPos(sharedPos);
//paint the item
...
}
protected:
void QGraphicsItem::mouseReleaseEvent ( QGraphicsSceneMouseEvent * event )
{
//get the position from the item that could have been moved
//you could also check if the position actually changed
sharedPosition->pos = pos();
}
private
Position *sharedPostion;
...
}
You would no have to create two items and give them both the same pointer to a Position object.
Item *item1 = new Item;
Item *item2 = new Item;
Position *sharedPos = new Position;
item1->setSharedPos(sharedPos);
item2->setSharedPos(sharedPos);
myScene1->addItem(item1);
myScene2->addItem(item2);
They should no at least share their position in the scenes.
If this works then you'll have to change the Position class to fit your needs and it should be working.
I am not quite shure if setting the position in the paint() function works. But thats how I would try to synchronise the items. If it does not work then you'll have to look for another place to update the settings of the items.
Or you could give the items a Pointer to each other and let them change the positions/settings directly.
e.g.
class Item : public QGraphicsItem
{
...
void QGraphicsItem::mouseReleaseEvent ( QGraphicsSceneMouseEvent * event )
{
otherItem->setPos(pos());
}
...
void setOtherItem(Item *item)
{
otherItem = item;
}
private:
Item *otherItem;
}
Item *item1 = new Item;
Item *item2 = new Item;
item1->setOtherItem(item2);
item2->setOtherItem(item1);
myScene1->addItem(item1);
myScene2->addItem(item2);